diff --git a/coral/src/app/features/configuration/environments/Kafka/KafkaEnvironments.test.tsx b/coral/src/app/features/configuration/environments/Kafka/KafkaEnvironments.test.tsx index bcf149558b..0edead790a 100644 --- a/coral/src/app/features/configuration/environments/Kafka/KafkaEnvironments.test.tsx +++ b/coral/src/app/features/configuration/environments/Kafka/KafkaEnvironments.test.tsx @@ -38,6 +38,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "DEV", envStatus: "OFFLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", otherParams: "", showDeleteEnv: false, totalNoPages: "1", @@ -66,6 +68,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "DEV_CLS", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", otherParams: "", showDeleteEnv: false, totalNoPages: "1", @@ -98,6 +102,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "TST", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", otherParams: "default.partitions=2,max.partitions=2,default.replication.factor=1,max.replication.factor=1,topic.prefix=,topic.suffix=", showDeleteEnv: false, @@ -131,6 +137,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "DEV", envStatus: "OFFLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", otherParams: "", showDeleteEnv: false, totalNoPages: "1", diff --git a/coral/src/app/features/configuration/environments/KafkaConnect/KafkaConnectEnvironments.test.tsx b/coral/src/app/features/configuration/environments/KafkaConnect/KafkaConnectEnvironments.test.tsx index 14e0113ec8..84dcfba3c3 100644 --- a/coral/src/app/features/configuration/environments/KafkaConnect/KafkaConnectEnvironments.test.tsx +++ b/coral/src/app/features/configuration/environments/KafkaConnect/KafkaConnectEnvironments.test.tsx @@ -38,6 +38,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "TST_CONNCT", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", @@ -55,6 +57,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "DEV", envStatus: "OFFLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", @@ -72,6 +76,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "UIKLAW", envStatus: "ONLINE", + envStatusTimeString: "21-Sep-2023 11:46:15", + envStatusTime: "2023-09-21T11:47:15.664615239", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", @@ -89,6 +95,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "UIKLAW", envStatus: "ONLINE", + envStatusTimeString: "21-Sep-2023 11:46:15", + envStatusTime: "2023-09-21T11:47:15.664615239", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", diff --git a/coral/src/app/features/configuration/environments/SchemaRegistry/SchemaRegistryEnvironments.test.tsx b/coral/src/app/features/configuration/environments/SchemaRegistry/SchemaRegistryEnvironments.test.tsx index 3c52adcb2f..99a22d53bc 100644 --- a/coral/src/app/features/configuration/environments/SchemaRegistry/SchemaRegistryEnvironments.test.tsx +++ b/coral/src/app/features/configuration/environments/SchemaRegistry/SchemaRegistryEnvironments.test.tsx @@ -38,6 +38,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "TST_SCHEMA", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", @@ -59,6 +61,8 @@ const mockedEnvironmentsResponse: EnvironmentPaginatedApiResponse = tenantName: "default", clusterName: "DEV_CLS", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", showDeleteEnv: false, totalNoPages: "1", currentPage: "1", diff --git a/coral/src/app/features/configuration/environments/components/EnvironmentStatus.test.tsx b/coral/src/app/features/configuration/environments/components/EnvironmentStatus.test.tsx index a77bc71544..2ba32ec793 100644 --- a/coral/src/app/features/configuration/environments/components/EnvironmentStatus.test.tsx +++ b/coral/src/app/features/configuration/environments/components/EnvironmentStatus.test.tsx @@ -89,6 +89,8 @@ describe("EnvironmentStatus", () => { mockGetUpdateEnvStatus.mockResolvedValue({ result: "success", envStatus: "ONLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", }); expect(screen.getByText("Working")).toBeVisible(); @@ -108,6 +110,8 @@ describe("EnvironmentStatus", () => { mockGetUpdateEnvStatus.mockResolvedValue({ result: "success", envStatus: "OFFLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", }); expect(screen.getByText("Working")).toBeVisible(); @@ -152,6 +156,8 @@ describe("EnvironmentStatus", () => { mockGetUpdateEnvStatus.mockResolvedValue({ result: "success", envStatus: "OFFLINE", + envStatusTime: "2023-09-21T11:47:15.664615239", + envStatusTimeString: "21-Sep-2023 11:46:15", }); expect(screen.getByText("Working")).toBeVisible(); diff --git a/coral/src/domain/environment/environment-test-helper.test.ts b/coral/src/domain/environment/environment-test-helper.test.ts index 61150a5764..b6981ee942 100644 --- a/coral/src/domain/environment/environment-test-helper.test.ts +++ b/coral/src/domain/environment/environment-test-helper.test.ts @@ -13,6 +13,8 @@ describe("environment-test-helper.ts", () => { defaultPartitions: undefined, defaultReplicationFactor: undefined, envStatus: "ONLINE", + envStatusTime: "2023-09-08T12:34:10.615919098", + envStatusTimeString: "21-Sep-2023 11:46:15", id: "1", maxPartitions: undefined, maxReplicationFactor: undefined, diff --git a/coral/src/domain/environment/environment-test-helper.ts b/coral/src/domain/environment/environment-test-helper.ts index ed3655ba68..72d1d28853 100644 --- a/coral/src/domain/environment/environment-test-helper.ts +++ b/coral/src/domain/environment/environment-test-helper.ts @@ -10,6 +10,8 @@ const defaultEnvironmentDTO: KlawApiModel<"EnvModelResponse"> = { tenantName: "default", clusterName: "DEV", envStatus: "ONLINE", + envStatusTimeString: "21-Sep-2023 11:46:15", + envStatusTime: "2023-09-08T12:34:10.615919098", otherParams: "default.partitions=2,max.partitions=2,default.replication.factor=1,max.replication.factor=1,topic.prefix=,topic.suffix=", showDeleteEnv: false, diff --git a/coral/src/services/api.ts b/coral/src/services/api.ts index dc439b3db9..4c1d005fa3 100644 --- a/coral/src/services/api.ts +++ b/coral/src/services/api.ts @@ -209,6 +209,8 @@ const API_PATHS = { | "getKafkaEnv" | "getKafkaConnectEnv" | "getSchemaRegEnv" + | "addEnvToCache" + | "removeEnvFromCache" >]: keyof ApiPaths; }; @@ -223,6 +225,14 @@ type GetTopicRequest = (params: { topicReqId: string }) => keyof ApiPaths; type GetKafkaEnv = (params: { envId: string }) => keyof ApiPaths; type GetConnectEnv = (params: { envId: string }) => keyof ApiPaths; type GetSchemaRegEnv = (params: { envId: string }) => keyof ApiPaths; +type AddEnvToCache = (params: { + tenantId: string; + id: string; +}) => keyof ApiPaths; +type RemoveEnvFromCache = (params: { + tenantId: string; + id: string; +}) => keyof ApiPaths; const DYNAMIC_API_PATHS = { getSchemaOfTopicFromSource: ({ @@ -242,6 +252,10 @@ const DYNAMIC_API_PATHS = { `/environments/kafkaconnect/${envId}` as keyof ApiPaths, getSchemaRegEnv: ({ envId }: Parameters[0]) => `/environments/schemaRegistry/${envId}` as keyof ApiPaths, + addEnvToCache: ({ tenantId }: Parameters[0]) => + `/cache/tenant/${tenantId}/entityType/environment` as keyof ApiPaths, + removeEnvFromCache: ({ tenantId, id }: Parameters[0]) => + `/cache/tenant/${tenantId}/entityType/environment/id/${id}` as keyof ApiPaths, } satisfies { [key in keyof Pick< ApiOperations, @@ -251,13 +265,17 @@ const DYNAMIC_API_PATHS = { | "getKafkaEnv" | "getKafkaConnectEnv" | "getSchemaRegEnv" + | "addEnvToCache" + | "removeEnvFromCache" >]: | GetSchemaOfTopicFromSource | GetSwitchTeams | GetTopicRequest | GetKafkaEnv | GetConnectEnv - | GetSchemaRegEnv; + | GetSchemaRegEnv + | AddEnvToCache + | RemoveEnvFromCache; }; type Params = URLSearchParams; diff --git a/coral/types/api.d.ts b/coral/types/api.d.ts index 3500c54386..32f9e89418 100644 --- a/coral/types/api.d.ts +++ b/coral/types/api.d.ts @@ -216,6 +216,9 @@ export type paths = { "/chPwd": { post: operations["changePwd"]; }; + "/cache/tenant/{tenantId}/entityType/environment": { + post: operations["addEnvToCache"]; + }; "/addTenantId": { post: operations["addTenantId"]; }; @@ -526,6 +529,9 @@ export type paths = { "/acl/request/{aclRequestId}": { get: operations["getAclRequest"]; }; + "/cache/tenant/{tenantId}/entityType/environment/id/{id}": { + delete: operations["removeEnvFromCache"]; + }; }; export type webhooks = Record; @@ -884,6 +890,41 @@ export type components = { envId: string; includeOnlyFailedTasks: boolean; }; + Env: { + id?: string; + /** Format: int32 */ + tenantId?: number; + name?: string; + stretchCode?: string; + /** Format: int32 */ + clusterId?: number; + type?: string; + otherParams?: string; + envExists?: string; + /** @enum {string} */ + envStatus?: "OFFLINE" | "ONLINE" | "NOT_KNOWN"; + /** Format: date-time */ + envStatusTime?: string; + envStatusTimeString?: string; + associatedEnv?: components["schemas"]["EnvTag"]; + params?: components["schemas"]["EnvParams"]; + }; + EnvParams: { + defaultPartitions?: string; + maxPartitions?: string; + partitionsList?: (string)[]; + defaultRepFactor?: string; + maxRepFactor?: string; + replicationFactorList?: (string)[]; + topicPrefix?: (string)[]; + topicSuffix?: (string)[]; + topicRegex?: (string)[]; + applyRegex?: boolean; + }; + EnvTag: { + id?: string; + name?: string; + }; KwTenantModel: { tenantName: string; tenantDesc: string; @@ -911,22 +952,6 @@ export type components = { tenantId?: number; params?: components["schemas"]["EnvParams"]; }; - EnvParams: { - defaultPartitions?: string; - maxPartitions?: string; - partitionsList?: (string)[]; - defaultRepFactor?: string; - maxRepFactor?: string; - replicationFactorList?: (string)[]; - topicPrefix?: (string)[]; - topicSuffix?: (string)[]; - topicRegex?: (string)[]; - applyRegex?: boolean; - }; - EnvTag: { - id?: string; - name?: string; - }; KwClustersModel: { /** Format: int32 */ clusterId?: number; @@ -1126,6 +1151,9 @@ export type components = { result: string; /** @enum {string} */ envStatus: "OFFLINE" | "ONLINE" | "NOT_KNOWN"; + /** Format: date-time */ + envStatusTime: string; + envStatusTimeString: string; }; TopicsCountPerEnv: { status?: string; @@ -1219,8 +1247,8 @@ export type components = { hasSchema: boolean; /** Format: int32 */ clusterId: number; - topicOwner?: boolean; highestEnv?: boolean; + topicOwner?: boolean; }; TopicBaseConfig: { topicName: string; @@ -1384,6 +1412,9 @@ export type components = { clusterName: string; /** @enum {string} */ envStatus: "OFFLINE" | "ONLINE" | "NOT_KNOWN"; + /** Format: date-time */ + envStatusTime: string; + envStatusTimeString: string; otherParams: string; showDeleteEnv: boolean; totalNoPages: string; @@ -2833,6 +2864,29 @@ export type operations = { }; }; }; + addEnvToCache: { + parameters: { + header: { + Authorization: string; + }; + path: { + tenantId: number; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Env"]; + }; + }; + responses: { + /** @description OK */ + 200: { + content: { + "application/json": components["schemas"]["ApiResponse"]; + }; + }; + }; + }; addTenantId: { requestBody: { content: { @@ -3731,7 +3785,7 @@ export type operations = { getEnvParams: { parameters: { query: { - envSelected: string; + envSelected: number; }; }; responses: { @@ -4354,4 +4408,23 @@ export type operations = { }; }; }; + removeEnvFromCache: { + parameters: { + header: { + Authorization: string; + }; + path: { + tenantId: number; + id: number; + }; + }; + responses: { + /** @description OK */ + 200: { + content: { + "application/json": components["schemas"]["ApiResponse"]; + }; + }; + }; + }; }; diff --git a/core/pom.xml b/core/pom.xml index f5d51fcfad..2101682cea 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -278,7 +278,10 @@ org.springframework.boot spring-boot-maven-plugin - -Dspringdoc.writer-with-default-pretty-printer=true + + -Dspringdoc.writer-with-default-pretty-printer=true + -Dklaw.core.ha.enable=true + @@ -287,8 +290,8 @@ stop - ${spring.integration.test.timeout} + diff --git a/core/src/main/java/io/aiven/klaw/config/CacheConfig.java b/core/src/main/java/io/aiven/klaw/config/CacheConfig.java index c608f0a302..6950ad9897 100644 --- a/core/src/main/java/io/aiven/klaw/config/CacheConfig.java +++ b/core/src/main/java/io/aiven/klaw/config/CacheConfig.java @@ -1,19 +1,38 @@ package io.aiven.klaw.config; import com.github.benmanes.caffeine.cache.Caffeine; +import io.aiven.klaw.constants.CacheConstants; +import io.aiven.klaw.dao.Env; +import io.aiven.klaw.service.HARestMessagingService; +import io.aiven.klaw.service.utils.CacheService; import java.time.Duration; import java.util.Arrays; +import java.util.Properties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration @EnableCaching public class CacheConfig { + @Autowired private HARestMessagingService HARestMessagingService; + + @Value("${klaw.clusterapi.access.username}") + private String apiUser; + + @Lazy @Autowired UserDetailsService userDetailsService; + @Primary public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); @@ -36,4 +55,39 @@ public CacheManager cacheManager() { cacheManager.setCaches(Arrays.asList(userCache, teamsCache)); return cacheManager; } + + @Bean + public CacheService kafkaEnvListPerTenant() { + return new CacheService<>(CacheConstants.ENVIRONMENT_PATH, HARestMessagingService); + } + + @Bean + public CacheService schemaRegEnvListPerTenant() { + return new CacheService<>(CacheConstants.ENVIRONMENT_PATH, HARestMessagingService); + } + + @Bean + public CacheService kafkaConnectEnvListPerTenant() { + return new CacheService<>(CacheConstants.ENVIRONMENT_PATH, HARestMessagingService); + } + + @Bean + public CacheService allEnvListPerTenant() { + return new CacheService<>(CacheConstants.ENVIRONMENT_PATH, HARestMessagingService); + } + + @Bean + @ConditionalOnProperty(name = "klaw.login.authentication.type", havingValue = "ad") + @Primary + public UserDetailsService getUserDetailsService() throws Exception { + final Properties globalUsers = new Properties(); + + try { + globalUsers.put(apiUser, ",CACHE_ADMIN,enabled"); + } catch (Exception e) { + throw new Exception("Error : User not loaded. Exiting."); + } + + return new InMemoryUserDetailsManager(globalUsers); + } } diff --git a/core/src/main/java/io/aiven/klaw/config/ConfigUtils.java b/core/src/main/java/io/aiven/klaw/config/ConfigUtils.java index e2bf3339e7..9ed4470482 100644 --- a/core/src/main/java/io/aiven/klaw/config/ConfigUtils.java +++ b/core/src/main/java/io/aiven/klaw/config/ConfigUtils.java @@ -42,7 +42,8 @@ protected static AntPathRequestMatcher[] getStaticResources(boolean coralEnabled "/resetMemoryCache/**", "/userActivation**", "/getActivationInfo**", - "/v3/**")); + "/v3/**", + "/cache/**")); if (coralEnabled) { staticResourcesHtmlArray.add("/assets/coral/**"); diff --git a/core/src/main/java/io/aiven/klaw/config/ManageDatabase.java b/core/src/main/java/io/aiven/klaw/config/ManageDatabase.java index 59c8cf503e..16c01db1f3 100644 --- a/core/src/main/java/io/aiven/klaw/config/ManageDatabase.java +++ b/core/src/main/java/io/aiven/klaw/config/ManageDatabase.java @@ -25,6 +25,7 @@ import io.aiven.klaw.model.requests.EnvModel; import io.aiven.klaw.model.response.EnvParams; import io.aiven.klaw.service.DefaultDataService; +import io.aiven.klaw.service.utils.CacheService; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -55,9 +56,6 @@ public class ManageDatabase implements ApplicationContextAware, InitializingBean public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Autowired HandleDbRequestsJdbc handleDbRequests; - - private static Map> envParamsMapPerTenant; - private static Map>> kwPropertiesMapPerTenant; private static Map> teamsPerTenant; @@ -101,10 +99,10 @@ public class ManageDatabase implements ApplicationContextAware, InitializingBean // key tenantId, sub key clusterid Pertenant private static Map> kwKafkaConnectClustersPertenant; private static Map> envMapPerTenant = new HashMap<>(); - private static Map> kafkaEnvListPerTenant = new HashMap<>(); - private static Map> schemaRegEnvListPerTenant = new HashMap<>(); - private static Map> kafkaConnectEnvListPerTenant = new HashMap<>(); - private static Map> allEnvListPerTenant = new HashMap<>(); + @Autowired private CacheService kafkaEnvListPerTenant; + @Autowired private CacheService schemaRegEnvListPerTenant; + @Autowired private CacheService kafkaConnectEnvListPerTenant; + @Autowired private CacheService allEnvListPerTenant; private static Map tenantConfig = new HashMap<>(); @@ -317,7 +315,7 @@ public List selectAllUsersInfo() { } public List getKafkaEnvListAllTenants(int tenantId) { - return kafkaEnvListPerTenant.get(tenantId); + return kafkaEnvListPerTenant.getCacheAsList(tenantId); } public Integer getAllTeamsSize() { @@ -357,31 +355,33 @@ public Optional getAssociatedSchemaEnvIdFromTopicId(String topicEnvId, i } public List getKafkaEnvList(int tenantId) { - if (kafkaEnvListPerTenant.get(tenantId).isEmpty()) { - return new ArrayList<>(); - } - return kafkaEnvListPerTenant.get(tenantId); + // Return a copy of the environments so no changes can be made to the cache + return kafkaEnvListPerTenant.getCacheAsList(tenantId); } public List getSchemaRegEnvList(int tenantId) { - if (schemaRegEnvListPerTenant.get(tenantId).isEmpty()) { - return new ArrayList<>(); - } - return schemaRegEnvListPerTenant.get(tenantId); + // Return a copy of the environments so no changes can be made to the cache + return schemaRegEnvListPerTenant.getCacheAsList(tenantId); } public List getKafkaConnectEnvList(int tenantId) { - if (kafkaConnectEnvListPerTenant.get(tenantId).isEmpty()) { - return new ArrayList<>(); - } - return kafkaConnectEnvListPerTenant.get(tenantId); + // Return a copy of the environments so no changes can be made to the cache + return kafkaConnectEnvListPerTenant.getCacheAsList(tenantId); } public List getAllEnvList(int tenantId) { - if (allEnvListPerTenant.get(tenantId).isEmpty()) { - return new ArrayList<>(); - } - return allEnvListPerTenant.get(tenantId); + // Return a copy of the environments so no changes can be made to the cache + return allEnvListPerTenant.getCacheAsList(tenantId); + } + + public Optional getEnv(int tenantId, Integer envId) { + // Return a copy of the environments so no changes can be made to the cache + return allEnvListPerTenant.get(tenantId, envId); + } + + public Optional getKafkaEnv(int tenantId, Integer envId) { + // Return a copy of the environments so no changes can be made to the cache + return kafkaEnvListPerTenant.get(tenantId, envId); } private Integer getTenantIdFromName(String tenantName) { @@ -392,8 +392,8 @@ private Integer getTenantIdFromName(String tenantName) { .getKey(); } - public Map getEnvParamsMap(Integer tenantId) { - return envParamsMapPerTenant.get(tenantId); + public EnvParams getEnvParams(Integer tenantId, Integer targetEnv) { + return kafkaEnvListPerTenant.get(tenantId, targetEnv).orElseGet(null).getParams(); } public List getTeamsAndAllowedEnvs(Integer teamId, int tenantId) { @@ -574,6 +574,49 @@ public void addTopicToCache(int tenantId, Topic topic) { } } + public void addEnvToCache(int tenantId, Env env, boolean isLocal) { + + switch (KafkaClustersType.of(env.getType())) { + case KAFKA -> kafkaEnvListPerTenant.addOrUpdate( + tenantId, Integer.valueOf(env.getId()), env, isLocal); + case SCHEMA_REGISTRY -> schemaRegEnvListPerTenant.addOrUpdate( + tenantId, Integer.valueOf(env.getId()), env, isLocal); + case KAFKA_CONNECT -> kafkaConnectEnvListPerTenant.addOrUpdate( + tenantId, Integer.valueOf(env.getId()), env, isLocal); + } + allEnvListPerTenant.addOrUpdate(tenantId, Integer.valueOf(env.getId()), env, isLocal); + updateTeamToEnvMappings(tenantId); + } + + private void updateTeamToEnvMappings(int tenantId) { + // TODO remove this + envsOfTenantsMap.put( + tenantId, + allEnvListPerTenant.getCache(tenantId).values().stream() + .map(Env::getId) + .collect(Collectors.toList())); + Map> teamsAndEnvs = new HashMap<>(); + teamsAndAllowedEnvsPerTenant.get(tenantId).keySet().stream() + .forEach(teamId -> teamsAndEnvs.put(teamId, envsOfTenantsMap.get(tenantId))); + teamsAndAllowedEnvsPerTenant.put(tenantId, teamsAndEnvs); + } + + public void removeEnvFromCache(int tenantId, int envId, boolean isLocal) { + Optional env = allEnvListPerTenant.get(tenantId, Integer.valueOf(envId)); + if (env.isEmpty()) { + return; + } + switch (KafkaClustersType.of(env.get().getType())) { + case KAFKA -> kafkaEnvListPerTenant.remove(tenantId, Integer.valueOf(envId), isLocal); + case SCHEMA_REGISTRY -> schemaRegEnvListPerTenant.remove( + tenantId, Integer.valueOf(envId), isLocal); + case KAFKA_CONNECT -> kafkaConnectEnvListPerTenant.remove( + tenantId, Integer.valueOf(envId), isLocal); + } + allEnvListPerTenant.remove(tenantId, Integer.valueOf(envId), isLocal); + updateTeamToEnvMappings(tenantId); + } + public List getTopicsForTenant(int tenantId) { return topicsPerTenant.get(tenantId); } @@ -759,7 +802,6 @@ private void loadRequestTypeStatuses() { } private void loadEnvironmentsMapForAllTenants() { - envParamsMapPerTenant = new HashMap<>(); for (Integer tenantId : tenantMap.keySet()) { loadEnvMapForOneTenant(tenantId); @@ -768,38 +810,26 @@ private void loadEnvironmentsMapForAllTenants() { } public void loadEnvMapForOneTenant(Integer tenantId) { - List kafkaEnvList = + Map kafkaEnvs = handleDbRequests.getAllKafkaEnvs(tenantId).stream() .filter(env -> "true".equals(env.getEnvExists())) - .collect(Collectors.toList()); - List schemaEnvList = + .collect(Collectors.toMap(e -> Integer.valueOf(e.getId()), Function.identity())); + Map schemaEnvs = handleDbRequests.getAllSchemaRegEnvs(tenantId).stream() .filter(env -> "true".equals(env.getEnvExists())) - .collect(Collectors.toList()); - List kafkaConnectEnvList = + .collect(Collectors.toMap(e -> Integer.valueOf(e.getId()), Function.identity())); + Map kafkaConnectEnvs = handleDbRequests.getAllKafkaConnectEnvs(tenantId).stream() .filter(env -> "true".equals(env.getEnvExists())) - .collect(Collectors.toList()); - List allEnvList = new ArrayList<>(); - allEnvList.addAll(kafkaEnvList); - allEnvList.addAll(schemaEnvList); - allEnvList.addAll(kafkaConnectEnvList); - - kafkaEnvListPerTenant.put(tenantId, kafkaEnvList); - schemaRegEnvListPerTenant.put(tenantId, schemaEnvList); - kafkaConnectEnvListPerTenant.put(tenantId, kafkaConnectEnvList); - allEnvListPerTenant.put(tenantId, allEnvList); - - List kafkaEnvTenantList = - kafkaEnvList.stream() - .filter(kafkaEnv -> Objects.equals(kafkaEnv.getTenantId(), tenantId)) - .toList(); - Map envParamsMap = new HashMap<>(); + .collect(Collectors.toMap(e -> Integer.valueOf(e.getId()), Function.identity())); - for (Env env : kafkaEnvTenantList) { - envParamsMap.put(env.getId(), env.getParams()); - } - envParamsMapPerTenant.put(tenantId, envParamsMap); + allEnvListPerTenant.addAll(tenantId, kafkaEnvs); + allEnvListPerTenant.addAll(tenantId, schemaEnvs); + allEnvListPerTenant.addAll(tenantId, kafkaConnectEnvs); + + kafkaEnvListPerTenant.addAll(tenantId, kafkaEnvs); + schemaRegEnvListPerTenant.addAll(tenantId, schemaEnvs); + kafkaConnectEnvListPerTenant.addAll(tenantId, kafkaConnectEnvs); // List allEnvs = handleDbRequests.getAllEnvs(tenantId); @@ -889,13 +919,18 @@ public String deleteTenant(int tenantId) { usersPerTenant.remove(tenantId); kwPropertiesMapPerTenant.remove(tenantId); rolesPermsMapPerTenant.remove(tenantId); - envParamsMapPerTenant.remove(tenantId); kwKafkaClustersPertenant.remove(tenantId); kwSchemaRegClustersPertenant.remove(tenantId); kwKafkaConnectClustersPertenant.remove(tenantId); kwAllClustersPertenant.remove(tenantId); + // delete EnvDetails + allEnvListPerTenant.removeCache(tenantId); + kafkaEnvListPerTenant.removeCache(tenantId); + schemaRegEnvListPerTenant.removeCache(tenantId); + kafkaConnectEnvListPerTenant.removeCache(tenantId); + return ApiResultStatus.SUCCESS.value; } } diff --git a/core/src/main/java/io/aiven/klaw/config/SecurityConfigNoSSO.java b/core/src/main/java/io/aiven/klaw/config/SecurityConfigNoSSO.java index 8fc15fffbf..c6cdb1f4b2 100644 --- a/core/src/main/java/io/aiven/klaw/config/SecurityConfigNoSSO.java +++ b/core/src/main/java/io/aiven/klaw/config/SecurityConfigNoSSO.java @@ -64,6 +64,9 @@ public class SecurityConfigNoSSO { @Value("${klaw.coral.enabled:false}") private boolean coralEnabled; + @Value("${klaw.core.app2app.username:KlawApp2App}") + private String apiUser; + @Autowired LdapTemplate ldapTemplate; private void shutdownApp() { @@ -151,6 +154,7 @@ public InMemoryUserDetailsManager inMemoryUserDetailsManager() throws Exception Iterator iter = users.iterator(); PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); loadAllUsers(globalUsers, iter, encoder); + globalUsers.put(apiUser, ",CACHE_ADMIN,enabled"); } return new InMemoryUserDetailsManager(globalUsers); } diff --git a/core/src/main/java/io/aiven/klaw/constants/CacheConstants.java b/core/src/main/java/io/aiven/klaw/constants/CacheConstants.java new file mode 100644 index 0000000000..fd19ac5c11 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/constants/CacheConstants.java @@ -0,0 +1,5 @@ +package io.aiven.klaw.constants; + +public class CacheConstants { + public static final String ENVIRONMENT_PATH = "environment"; +} diff --git a/core/src/main/java/io/aiven/klaw/controller/CacheController.java b/core/src/main/java/io/aiven/klaw/controller/CacheController.java new file mode 100644 index 0000000000..44e83bf238 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/controller/CacheController.java @@ -0,0 +1,55 @@ +package io.aiven.klaw.controller; + +import io.aiven.klaw.config.ManageDatabase; +import io.aiven.klaw.dao.Env; +import io.aiven.klaw.error.KlawNotAuthorizedException; +import io.aiven.klaw.model.ApiResponse; +import io.aiven.klaw.service.HARestMessagingService; +import io.aiven.klaw.service.JwtTokenUtilService; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@ConditionalOnProperty(prefix = "klaw.core.ha", name = "enable", matchIfMissing = false) +@RequestMapping("/cache") +@Slf4j +public class CacheController { + + @Autowired private ManageDatabase manageDatabase; + + @Autowired JwtTokenUtilService jwtTokenUtilService; + + @PostMapping( + value = "/tenant/{tenantId}/entityType/environment", + produces = {MediaType.APPLICATION_JSON_VALUE}, + consumes = {MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity addEnvToCache( + @PathVariable("tenantId") Integer tenantId, + @Valid @RequestBody Env env, + @RequestHeader(name = "Authorization") String token) + throws KlawNotAuthorizedException { + jwtTokenUtilService.validateRole(token, HARestMessagingService.CACHE_ADMIN); + manageDatabase.addEnvToCache(tenantId, env, true); + return new ResponseEntity<>(HttpStatus.OK); + } + + @DeleteMapping( + value = "/tenant/{tenantId}/entityType/environment/id/{id}", + produces = {MediaType.APPLICATION_JSON_VALUE}, + consumes = {MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity removeEnvFromCache( + @PathVariable("tenantId") Integer tenantId, + @PathVariable("id") Integer id, + @RequestHeader(name = "Authorization") String token) + throws KlawNotAuthorizedException { + jwtTokenUtilService.validateRole(token, HARestMessagingService.CACHE_ADMIN); + manageDatabase.removeEnvFromCache(tenantId, id, true); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/core/src/main/java/io/aiven/klaw/controller/EnvsClustersTenantsController.java b/core/src/main/java/io/aiven/klaw/controller/EnvsClustersTenantsController.java index a86e6afecf..339c723b89 100644 --- a/core/src/main/java/io/aiven/klaw/controller/EnvsClustersTenantsController.java +++ b/core/src/main/java/io/aiven/klaw/controller/EnvsClustersTenantsController.java @@ -1,5 +1,6 @@ package io.aiven.klaw.controller; +import io.aiven.klaw.error.KlawBadRequestException; import io.aiven.klaw.error.KlawException; import io.aiven.klaw.error.KlawValidationException; import io.aiven.klaw.model.ApiResponse; @@ -160,7 +161,7 @@ public ResponseEntity> getSyncEnv() { method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity getEnvParams( - @RequestParam(value = "envSelected") String envSelected) { + @RequestParam(value = "envSelected") Integer envSelected) { return new ResponseEntity<>( envsClustersTenantsControllerService.getEnvParams(envSelected), HttpStatus.OK); } @@ -388,7 +389,7 @@ public ResponseEntity getKwPubkey() { method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity getUpdateEnvStatus( - @RequestParam(value = "envId") String envId) throws KlawException { + @RequestParam(value = "envId") String envId) throws KlawBadRequestException { return new ResponseEntity<>( envsClustersTenantsControllerService.getUpdateEnvStatus(envId), HttpStatus.OK); } diff --git a/core/src/main/java/io/aiven/klaw/dao/Env.java b/core/src/main/java/io/aiven/klaw/dao/Env.java index 4afb49876b..ea3d564495 100644 --- a/core/src/main/java/io/aiven/klaw/dao/Env.java +++ b/core/src/main/java/io/aiven/klaw/dao/Env.java @@ -4,15 +4,9 @@ import io.aiven.klaw.helpers.EnvTagConverter; import io.aiven.klaw.model.enums.ClusterStatus; import io.aiven.klaw.model.response.EnvParams; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; +import jakarta.persistence.*; import java.io.Serializable; +import java.time.LocalDateTime; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -55,6 +49,10 @@ public class Env implements Serializable { @Enumerated(EnumType.STRING) private ClusterStatus envStatus; + @Transient private LocalDateTime envStatusTime; + + @Transient private String envStatusTimeString; + @Convert(converter = EnvTagConverter.class) @Column(name = "associatedenv") private EnvTag associatedEnv; diff --git a/core/src/main/java/io/aiven/klaw/model/response/EnvModelResponse.java b/core/src/main/java/io/aiven/klaw/model/response/EnvModelResponse.java index bc16f06fae..24bf86df21 100644 --- a/core/src/main/java/io/aiven/klaw/model/response/EnvModelResponse.java +++ b/core/src/main/java/io/aiven/klaw/model/response/EnvModelResponse.java @@ -5,6 +5,7 @@ import io.aiven.klaw.model.enums.KafkaClustersType; import jakarta.validation.constraints.NotNull; import java.io.Serializable; +import java.time.LocalDateTime; import java.util.List; import lombok.Getter; import lombok.Setter; @@ -31,6 +32,10 @@ public class EnvModelResponse implements Serializable { @NotNull private ClusterStatus envStatus; + @NotNull private LocalDateTime envStatusTime; + + @NotNull private String envStatusTimeString; + @NotNull private String otherParams; @NotNull private boolean showDeleteEnv; diff --git a/core/src/main/java/io/aiven/klaw/model/response/EnvUpdatedStatus.java b/core/src/main/java/io/aiven/klaw/model/response/EnvUpdatedStatus.java index 566971559e..d10e6d839d 100644 --- a/core/src/main/java/io/aiven/klaw/model/response/EnvUpdatedStatus.java +++ b/core/src/main/java/io/aiven/klaw/model/response/EnvUpdatedStatus.java @@ -2,6 +2,7 @@ import io.aiven.klaw.model.enums.ClusterStatus; import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; import lombok.Data; @Data @@ -9,4 +10,8 @@ public class EnvUpdatedStatus { @NotNull private String result; @NotNull private ClusterStatus envStatus; + + @NotNull private LocalDateTime envStatusTime; + + @NotNull private String envStatusTimeString; } diff --git a/core/src/main/java/io/aiven/klaw/service/BaseOverviewService.java b/core/src/main/java/io/aiven/klaw/service/BaseOverviewService.java index e18d6abb99..b3d1cc8cb8 100644 --- a/core/src/main/java/io/aiven/klaw/service/BaseOverviewService.java +++ b/core/src/main/java/io/aiven/klaw/service/BaseOverviewService.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -326,10 +325,6 @@ protected Object getPrincipal() { } protected Env getEnvDetails(String envId, int tenantId) { - Optional envFound = - manageDatabase.getAllEnvList(tenantId).stream() - .filter(env -> Objects.equals(env.getId(), envId)) - .findFirst(); - return envFound.orElse(null); + return manageDatabase.getEnv(tenantId, Integer.valueOf(envId)).orElse(null); } } diff --git a/core/src/main/java/io/aiven/klaw/service/CommonUtilsService.java b/core/src/main/java/io/aiven/klaw/service/CommonUtilsService.java index 77a20c4ecf..6f4a7c042b 100644 --- a/core/src/main/java/io/aiven/klaw/service/CommonUtilsService.java +++ b/core/src/main/java/io/aiven/klaw/service/CommonUtilsService.java @@ -25,7 +25,6 @@ import io.aiven.klaw.model.enums.RequestOperationType; import io.aiven.klaw.model.requests.ResetEntityCache; import java.io.*; -import java.net.InetAddress; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -38,7 +37,6 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -54,9 +52,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.env.Environment; import org.springframework.http.*; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -74,9 +70,6 @@ @Slf4j public class CommonUtilsService { - public static final String BASE_URL_ADDRESS = "BASE_URL_ADDRESS"; - public static final String BASE_URL_NAME = "BASE_URL_NAME"; - public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); @@ -109,14 +102,9 @@ public class CommonUtilsService { @Value("${klaw.login.authentication.type}") private String authenticationType; - @Autowired Environment environment; - @Autowired ManageDatabase manageDatabase; - private static Map baseUrlsMap; - - private static HttpComponentsClientHttpRequestFactory requestFactory = - ClusterApiService.requestFactory; + @Autowired HARestMessagingService HARestMessagingService; @Value("${klaw.uiapi.servers:server1,server2}") private String uiApiServers; @@ -128,8 +116,7 @@ public class CommonUtilsService { private InMemoryUserDetailsManager inMemoryUserDetailsManager; private RestTemplate getRestTemplate() { - if (uiApiServers.toLowerCase().startsWith("https")) return new RestTemplate(requestFactory); - else return new RestTemplate(); + return HARestMessagingService.getRestTemplate(); } public Authentication getAuthentication() { @@ -417,21 +404,6 @@ public String getBaseUrl() { + kwContextPath; } - public Map getBaseIpUrlFromEnvironment() { - if (baseUrlsMap != null && !baseUrlsMap.isEmpty()) { - return baseUrlsMap; - } else { - baseUrlsMap = new HashMap<>(); - String hostAddress = InetAddress.getLoopbackAddress().getHostAddress(); - String hostName = InetAddress.getLoopbackAddress().getHostName(); - int port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port"))); - - baseUrlsMap.put(BASE_URL_ADDRESS, hostAddress + ":" + port); - baseUrlsMap.put(BASE_URL_NAME, hostName + ":" + port); - return baseUrlsMap; - } - } - public void resetCacheOnOtherServers(KwMetadataUpdates kwMetadataUpdates) { log.info("invokeResetEndpoints"); try { @@ -446,18 +418,9 @@ public void resetCacheOnOtherServers(KwMetadataUpdates kwMetadataUpdates) { basePath = server + "/" + kwContextPath; } - Map baseUrlsFromEnv = getBaseIpUrlFromEnvironment(); - // ignore metadata cache reset on local. - if (baseUrlsFromEnv != null && !baseUrlsFromEnv.isEmpty()) { - if (baseUrlsFromEnv.containsKey(BASE_URL_ADDRESS) - && basePath.contains(baseUrlsFromEnv.get(BASE_URL_ADDRESS))) { - continue; - } - if (baseUrlsFromEnv.containsKey(BASE_URL_NAME) - && basePath.contains(baseUrlsFromEnv.get(BASE_URL_NAME))) { - continue; - } + if (HARestMessagingService.isLocalServerUrl(basePath)) { + continue; } if (kwMetadataUpdates.getEntityValue() == null) { @@ -771,10 +734,6 @@ public boolean isCreateNewSchemaAllowed(String schemaEnvId, int tenantId) { } public Env getEnvDetails(String envId, int tenantId) { - Optional envFound = - manageDatabase.getKafkaEnvList(tenantId).stream() - .filter(env -> Objects.equals(env.getId(), envId)) - .findFirst(); - return envFound.orElse(null); + return manageDatabase.getKafkaEnv(tenantId, Integer.valueOf(envId)).orElse(null); } } diff --git a/core/src/main/java/io/aiven/klaw/service/EnvsClustersTenantsControllerService.java b/core/src/main/java/io/aiven/klaw/service/EnvsClustersTenantsControllerService.java index 7d914fe345..94f0eb526e 100644 --- a/core/src/main/java/io/aiven/klaw/service/EnvsClustersTenantsControllerService.java +++ b/core/src/main/java/io/aiven/klaw/service/EnvsClustersTenantsControllerService.java @@ -29,6 +29,7 @@ import io.aiven.klaw.dao.KwClusters; import io.aiven.klaw.dao.KwTenants; import io.aiven.klaw.dao.UserInfo; +import io.aiven.klaw.error.KlawBadRequestException; import io.aiven.klaw.error.KlawException; import io.aiven.klaw.error.KlawValidationException; import io.aiven.klaw.helpers.HandleDbRequests; @@ -60,6 +61,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -462,10 +465,8 @@ private List getEnvModels( return envModelList; } - public EnvParams getEnvParams(String targetEnv) { - return manageDatabase - .getEnvParamsMap(commonUtilsService.getTenantId(getUserName())) - .get(targetEnv); + public EnvParams getEnvParams(Integer targetEnv) { + return manageDatabase.getEnvParams(commonUtilsService.getTenantId(getUserName()), targetEnv); } public List getSchemaRegEnvs() { @@ -641,8 +642,7 @@ public ApiResponse addNewEnv(EnvModel newEnv) throws KlawException, KlawValidati env.setAssociatedEnv(envTag); String result = manageDatabase.getHandleDbRequests().addNewEnv(env); if (result.equals(ApiResultStatus.SUCCESS.value)) { - commonUtilsService.updateMetadata( - tenantId, EntityType.ENVIRONMENT, MetadataOperationType.CREATE, null); + manageDatabase.addEnvToCache(tenantId, env, false); return ApiResponse.ok(result); } else { return ApiResponse.notOk(result); @@ -853,8 +853,7 @@ public ApiResponse deleteEnvironment(String envId, String envType) throws KlawEx String result = manageDatabase.getHandleDbRequests().deleteEnvironmentRequest(envId, tenantId); if (result.equals(ApiResultStatus.SUCCESS.value)) { - commonUtilsService.updateMetadata( - tenantId, EntityType.ENVIRONMENT, MetadataOperationType.DELETE, null); + manageDatabase.removeEnvFromCache(tenantId, Integer.valueOf(envId), false); return ApiResponse.ok(result); } else { return ApiResponse.notOk(result); @@ -926,6 +925,8 @@ private void removeAssociationWithKafkaEnv(EnvTag envTag, String envId, int tena .getEnvDetails(existingEnv.getAssociatedEnv().getId(), tenantId); linkedEnv.setAssociatedEnv(null); manageDatabase.getHandleDbRequests().addNewEnv(linkedEnv); + // add to cache + manageDatabase.addEnvToCache(tenantId, linkedEnv, false); } } @@ -943,6 +944,8 @@ private void associateWithKafkaEnv(EnvTag envTag, String envId, String envName, } linkedEnv.setAssociatedEnv(new EnvTag(envId, envName)); manageDatabase.getHandleDbRequests().addNewEnv(linkedEnv); + // add update to cache + manageDatabase.addEnvToCache(tenantId, linkedEnv, false); } private String getUserName() { @@ -1245,24 +1248,33 @@ public KwReport getPublicKey() { return kwPublicKey; } - public EnvUpdatedStatus getUpdateEnvStatus(String envId) throws KlawException { + public EnvUpdatedStatus getUpdateEnvStatus(String envId) throws KlawBadRequestException { + EnvUpdatedStatus envUpdatedStatus = new EnvUpdatedStatus(); int tenantId = commonUtilsService.getTenantId(getUserName()); - Env env = manageDatabase.getHandleDbRequests().getEnvDetails(envId, tenantId); + List allEnvs = manageDatabase.getAllEnvList(tenantId); + Optional env = + allEnvs.stream() + .filter(e -> e.getId().equals(envId) && e.getTenantId().equals(tenantId)) + .findFirst(); + + if (env.isEmpty()) { + throw new KlawBadRequestException("No Such environment."); + } ClusterStatus status; KwClusters kwClusters = null; kwClusters = manageDatabase - .getClusters(KafkaClustersType.of(env.getType()), tenantId) - .get(env.getClusterId()); + .getClusters(KafkaClustersType.of(env.get().getType()), tenantId) + .get(env.get().getClusterId()); try { status = clusterApiService.getKafkaClusterStatus( kwClusters.getBootstrapServers(), kwClusters.getProtocol(), kwClusters.getClusterName() + kwClusters.getClusterId(), - env.getType(), + env.get().getType(), kwClusters.getKafkaFlavor(), tenantId); @@ -1270,14 +1282,22 @@ public EnvUpdatedStatus getUpdateEnvStatus(String envId) throws KlawException { status = ClusterStatus.OFFLINE; log.error("Error from getUpdateEnvStatus ", e); } - env.setEnvStatus(status); + LocalDateTime statusTime = LocalDateTime.now(ZoneOffset.UTC); + env.get().setEnvStatus(status); + env.get().setEnvStatusTime(statusTime); + env.get().setEnvStatusTimeString(DATE_TIME_DDMMMYYYY_HHMMSS_FORMATTER.format(statusTime)); + + // Is this required can we remove it? kwClusters.setClusterStatus(status); manageDatabase.getHandleDbRequests().addNewCluster(kwClusters); - manageDatabase.getHandleDbRequests().addNewEnv(env); - manageDatabase.loadEnvMapForOneTenant(tenantId); + + manageDatabase.addEnvToCache(tenantId, env.get(), false); envUpdatedStatus.setResult(ApiResultStatus.SUCCESS.value); envUpdatedStatus.setEnvStatus(status); + envUpdatedStatus.setEnvStatusTime(statusTime); + envUpdatedStatus.setEnvStatusTimeString( + DATE_TIME_DDMMMYYYY_HHMMSS_FORMATTER.format(statusTime)); return envUpdatedStatus; } diff --git a/core/src/main/java/io/aiven/klaw/service/HARestMessagingService.java b/core/src/main/java/io/aiven/klaw/service/HARestMessagingService.java new file mode 100644 index 0000000000..3e78956e60 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/service/HARestMessagingService.java @@ -0,0 +1,220 @@ +package io.aiven.klaw.service; + +import static io.aiven.klaw.error.KlawErrorMessages.CLUSTER_API_ERR_117; + +import io.aiven.klaw.error.KlawException; +import io.aiven.klaw.model.ApiResponse; +import io.aiven.klaw.service.interfaces.HAMessagingServiceI; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.net.InetAddress; +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Service +@Slf4j +public class HARestMessagingService implements HAMessagingServiceI { + + public static final String URL_SEPERATOR = "/"; + public static final String TENANT = "tenant"; + public static final String ID = "id"; + public static final String BEARER = "Bearer "; + public static final String AUTHORIZATION = "Authorization"; + public static final String ROLES = "Roles"; + public static final String NAME = "name"; + public static final String ENTITY_TYPE = "entityType"; + public static final String CACHE = "cache"; + public static final String CACHE_ADMIN = "CACHE_ADMIN"; + public static final String APP_2_APP = "App2App"; + @Autowired Environment environment; + + private List clusterUrls; + + private RestTemplate rest; + private static HttpComponentsClientHttpRequestFactory requestFactory = null; + + private static Map baseUrlsMap; + + @Value("${klaw.core.app2app.base64.secret:#{''}}") + private String app2AppApiKey; + + @Value("${klaw.core.app2app.username:KlawApp2App}") + private String apiUser; + + @Value("${klaw.uiapi.servers:server1,server2}") + private String clusterUrlsAsString; + + public static final String BASE_URL_ADDRESS = "BASE_URL_ADDRESS"; + public static final String BASE_URL_NAME = "BASE_URL_NAME"; + + private Map getBaseIpUrlFromEnvironment() { + if (baseUrlsMap != null && !baseUrlsMap.isEmpty()) { + return baseUrlsMap; + } else { + baseUrlsMap = new HashMap<>(); + String hostAddress = InetAddress.getLoopbackAddress().getHostAddress(); + String hostName = InetAddress.getLoopbackAddress().getHostName(); + int port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port"))); + + baseUrlsMap.put(BASE_URL_ADDRESS, hostAddress + ":" + port); + baseUrlsMap.put(BASE_URL_NAME, hostName + ":" + port); + return baseUrlsMap; + } + } + + public boolean isLocalServerUrl(String basePath) { + Map baseUrlsFromEnv = getBaseIpUrlFromEnvironment(); + if (baseUrlsFromEnv != null && !baseUrlsFromEnv.isEmpty()) { + if (existsInMap(basePath, baseUrlsFromEnv, BASE_URL_ADDRESS)) { + return true; + } + return existsInMap(basePath, baseUrlsFromEnv, BASE_URL_NAME); + } + return false; + } + + private static boolean existsInMap( + String basePath, Map baseUrlsFromEnv, String key) { + return baseUrlsFromEnv.containsKey(key) && basePath.contains(baseUrlsFromEnv.get(key)); + } + + public RestTemplate getRestTemplate() { + if (clusterUrlsAsString.toLowerCase().startsWith("https")) { + if (requestFactory == null) { + requestFactory = ClusterApiService.requestFactory; + } + return new RestTemplate(requestFactory); + } else { + return new RestTemplate(); + } + } + + public List getHAClusterUrls() { + if (clusterUrlsAsString != null && !clusterUrlsAsString.isEmpty()) { + return Arrays.stream(clusterUrlsAsString.split(",")) + .filter(serverUrl -> !isLocalServerUrl(serverUrl)) + .toList(); + } else { + return new ArrayList<>(); + } + } + + public HttpHeaders createHeaders() throws KlawException { + HttpHeaders httpHeaders = new HttpHeaders(); + String authHeader = BEARER + generateToken(apiUser); + httpHeaders.set(AUTHORIZATION, authHeader); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + + return httpHeaders; + } + + private String generateToken(String username) throws KlawException { + if (app2AppApiKey.isBlank()) { + log.error(CLUSTER_API_ERR_117); + throw new KlawException(CLUSTER_API_ERR_117); + } + + Key hmacKey = + new SecretKeySpec( + Base64.decodeBase64(app2AppApiKey), SignatureAlgorithm.HS256.getJcaName()); + Instant now = Instant.now(); + + return Jwts.builder() + .claim(NAME, username) + .claim(ROLES, List.of(CACHE_ADMIN, APP_2_APP)) + .setSubject(username) + .setId(UUID.randomUUID().toString()) + .setIssuedAt(Date.from(now)) + .setExpiration(Date.from(now.plus(3L, ChronoUnit.MINUTES))) // expiry in 3 minutes + .signWith(hmacKey) + .compact(); + } + + @Override + public void sendUpdate(String entityType, int tenantId, Object entry) { + if (isLazyLoaded()) { + throw new RuntimeException("Unable to load High Availability Cache"); + } + + for (String url : clusterUrls) { + try { + HttpEntity request = new HttpEntity<>(entry, createHeaders()); + rest.postForObject(getUrl(url, entityType, tenantId, null), request, ApiResponse.class); + + } catch (KlawException | RestClientException clientException) { + log.error( + "Exception while sending HA updates to another instance {} in the cluster.", + url, + clientException); + } + } + } + + @Override + public void sendRemove(String entityType, int tenantId, int id) { + if (isLazyLoaded()) { + throw new RuntimeException("Unable to load High Availability Cache"); + } + + for (String url : clusterUrls) { + try { + rest.exchange( + getUrl(url, entityType, tenantId, id), + HttpMethod.DELETE, + new HttpEntity<>(createHeaders()), + Void.class); + } catch (RestClientException | KlawException clientException) { + log.error( + "Exception while sending HA updates to another instance {} in the cluster.", + url, + clientException); + } + } + } + + private boolean isLazyLoaded() { + + if (rest == null || clusterUrls == null) { + this.rest = getRestTemplate(); + this.clusterUrls = getHAClusterUrls(); + } + return (rest == null && clusterUrls == null); + } + + private String getUrl(String url, String entityType, int tenantId, Integer id) { + if (url.endsWith(URL_SEPERATOR)) { + url = url.substring(0, url.length() - 1); + } + if (id != null) { + return String.join( + URL_SEPERATOR, + url, + CACHE, + TENANT, + Integer.toString(tenantId), + ENTITY_TYPE, + entityType, + ID, + Integer.toString(id)); + } else { + return String.join( + URL_SEPERATOR, url, CACHE, TENANT, Integer.toString(tenantId), ENTITY_TYPE, entityType); + } + } +} diff --git a/core/src/main/java/io/aiven/klaw/service/JwtTokenUtilService.java b/core/src/main/java/io/aiven/klaw/service/JwtTokenUtilService.java new file mode 100644 index 0000000000..a8562dc4b9 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/service/JwtTokenUtilService.java @@ -0,0 +1,90 @@ +package io.aiven.klaw.service; + +import io.aiven.klaw.error.KlawNotAuthorizedException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.security.Key; +import java.util.Date; +import java.util.List; +import java.util.function.Function; +import javax.crypto.spec.SecretKeySpec; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty(prefix = "klaw.core.ha", name = "enable") +public class JwtTokenUtilService implements InitializingBean { + @Value("${klaw.core.app2app.base64.secret:#{''}}") + private String app2AppSecret; + + public static final String BEARER = "Bearer "; + @Autowired UserDetailsService userDetailsService; + private static byte[] decodedSecret; + + // retrieve username from jwt token + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + // retrieve expiration date from jwt token + private Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + private T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + // for retrieving any information from token we will need the secret key + public Claims getAllClaimsFromToken(String token) { + Key hmacKey = new SecretKeySpec(decodedSecret, SignatureAlgorithm.HS256.getJcaName()); + Jws jwt = Jwts.parserBuilder().setSigningKey(hmacKey).build().parseClaimsJws(token); + return jwt.getBody(); + } + + // check if the token has expired + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + // validate token + public Boolean validateToken(String token) { + return !isTokenExpired(token); + } + + public void validateRole(String token, String... expectedRole) throws KlawNotAuthorizedException { + + token = token.substring(BEARER.length()).trim(); + Claims claims = getAllClaimsFromToken(token); + UserDetails user = userDetailsService.loadUserByUsername(getUsernameFromToken(token)); + if (user == null + && !validateToken(token) + && !claims.get("Roles", List.class).containsAll(List.of(expectedRole))) { + throw new KlawNotAuthorizedException("UnAuthorized"); + } + } + + // validate secret during app initialization + @Override + public void afterPropertiesSet() throws Exception { + if (app2AppSecret != null && !app2AppSecret.trim().isEmpty()) { + try { + decodedSecret = Base64.decodeBase64(app2AppSecret); + return; + } catch (Exception e) { + throw new Exception("Invalid Base64 value configured. klaw.core.app2app.base64.secret"); + } + } + throw new Exception("Property not configured. klaw.core.app2app.base64.secret"); + } +} diff --git a/core/src/main/java/io/aiven/klaw/service/interfaces/HAMessagingServiceI.java b/core/src/main/java/io/aiven/klaw/service/interfaces/HAMessagingServiceI.java new file mode 100644 index 0000000000..ff546a98f6 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/service/interfaces/HAMessagingServiceI.java @@ -0,0 +1,8 @@ +package io.aiven.klaw.service.interfaces; + +public interface HAMessagingServiceI { + + void sendUpdate(String entityType, int tenantId, Object entry); + + void sendRemove(String entityType, int tenantId, int id); +} diff --git a/core/src/main/java/io/aiven/klaw/service/utils/CacheService.java b/core/src/main/java/io/aiven/klaw/service/utils/CacheService.java new file mode 100644 index 0000000000..c9435a8c47 --- /dev/null +++ b/core/src/main/java/io/aiven/klaw/service/utils/CacheService.java @@ -0,0 +1,97 @@ +package io.aiven.klaw.service.utils; + +import io.aiven.klaw.service.interfaces.HAMessagingServiceI; +import java.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; + +/** + * This cache provides a standardised way with built in inter instance update to other caches when + * working in a multi cluster deployment. Standard querying mechanisms are provided to ensure that + * the most efficient use of the cache is used along with the ability to use Java Streams with the + * output for complex queries. + * + * @param The Entity Type that is to be stored in this Cache. + */ +@Slf4j +public class CacheService { + + Map> cache; + + private final String urlEndpoint; + private final String entityType; + + private final HAMessagingServiceI utilsService; + + public CacheService(String entityType, HAMessagingServiceI utilsService) { + // TODO an interface needs to be added to allow the passing in of different communication + // methods (kafka/https/rabbitmq etc) to allow any org to use what they want for maintaining + // cache. + this.cache = new HashMap<>(); + this.urlEndpoint = "cache"; + this.entityType = entityType; + this.utilsService = utilsService; + } + + public T add(int tenantId, Integer id, T entry) { + return addOrUpdate(tenantId, id, entry, false); + } + + public void addAll(int tenantId, Map entries) { + if (entries == null) { + entries = new HashMap<>(); + } + getCache(tenantId).putAll(entries); + } + + public T remove(int tenantId, Integer id, boolean isLocalUpdate) { + log.debug("remove id {}", id); + if (!isLocalUpdate) { + sendHighAvailabilityRemove(tenantId, id); + } + return getCache(tenantId).remove(id); + } + + public Map removeCache(int tenantId) { + return cache.remove(tenantId); + } + + public T update(int tenantId, Integer id, T entry) { + return addOrUpdate(tenantId, id, entry, false); + } + + public T addOrUpdate(int tenantId, Integer id, T entry, boolean isLocalUpdate) { + log.debug("addOrUpdate {}", entry); + getCache(tenantId).put(id, entry); + if (!isLocalUpdate) { + sendHighAvailabilityUpdate(tenantId, entry); + } + return entry; + } + + public Optional get(int tenantId, Integer id) { + return Optional.ofNullable(getCache(tenantId).get(id)); + } + + public Map getCache(Integer tenantId) { + if (!cache.containsKey(tenantId)) { + cache.put(tenantId, new HashMap<>()); + } + return cache.get(tenantId); + } + + public List getCacheAsList(Integer tenantId) { + if (!cache.containsKey(tenantId)) { + cache.put(tenantId, new HashMap<>()); + } + return new ArrayList<>(cache.get(tenantId).values()); + } + + private void sendHighAvailabilityUpdate(int tenantId, T entry) { + utilsService.sendUpdate(entityType, tenantId, entry); + } + + private void sendHighAvailabilityRemove(int tenantId, Integer id) { + utilsService.sendRemove(entityType, tenantId, id); + } +} diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties index c9507432a3..8e9cf6acb9 100644 --- a/core/src/main/resources/application.properties +++ b/core/src/main/resources/application.properties @@ -80,7 +80,15 @@ klaw.enable.sso=false # Uncomment the below to establish connectivity between core and cluster apis. # Provide a base 64 encoded string. The same secret should be configured in Klaw Cluster Api. klaw.clusterapi.access.base64.secret= - +# This allows App2App communication in a HA setup +# where multiple Klaw-core instances are deployed +# Provide a base 64 encoded string. The same secret should be used in all Instances. +# Update the below to your own unique secret!!!!! +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== +klaw.core.app2app.username=KlawApp2App +# +# Enable High Availability for multi-instance Klaw-core deployment. +klaw.core.ha.enable=false # In case of AD or Azure AD, configure an existing user from AD in the below config for username. Ex : superadmin@domain. # Leave it blank if this user is not required klaw.superadmin.default.username=superadmin diff --git a/core/src/test/java/io/aiven/klaw/service/HARestMessagingServiceTest.java b/core/src/test/java/io/aiven/klaw/service/HARestMessagingServiceTest.java new file mode 100644 index 0000000000..17c55aa08d --- /dev/null +++ b/core/src/test/java/io/aiven/klaw/service/HARestMessagingServiceTest.java @@ -0,0 +1,122 @@ +package io.aiven.klaw.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import io.aiven.klaw.UtilMethods; +import io.aiven.klaw.dao.Env; +import io.aiven.klaw.model.ApiResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +@ExtendWith(SpringExtension.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class HARestMessagingServiceTest { + public static final String KLAW_PROJECT_IO_9097 = "https://klaw-project.io:9097"; + private UtilMethods utilMethods; + @Mock private Environment environment; + + @Mock HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory; + + @Mock RestTemplate rest; + + private HARestMessagingService restMessagingService; + + @BeforeEach + void setUp() { + utilMethods = new UtilMethods(); + restMessagingService = new HARestMessagingService(); + ReflectionTestUtils.setField(restMessagingService, "environment", environment); + ReflectionTestUtils.setField(restMessagingService, "apiUser", "MyApp2AppUser"); + ReflectionTestUtils.setField( + restMessagingService, + "clusterUrlsAsString", + "https://localhost:0,https://klaw-project.io:9097"); + ReflectionTestUtils.setField( + restMessagingService, + "app2AppApiKey", + "TXlCaWdnZXN0U2VjcmV0SXNUaGF0S2xhd0lzUmVhbGx5RXhjZWxsZW50="); + // ensure that the base urls static map is reset after every test + ReflectionTestUtils.setField(restMessagingService, "baseUrlsMap", null); + ReflectionTestUtils.setField(restMessagingService, "rest", null); + ClusterApiService.requestFactory = httpComponentsClientHttpRequestFactory; + } + + @Test + public void test_ClusterUrls_FilterLocalUrl() { + when(environment.getProperty("server.port")).thenReturn("0"); + List clusterUrls = restMessagingService.getHAClusterUrls(); + // Should only contain the serverUrls that arent the current server. + assertThat(clusterUrls).containsOnly(KLAW_PROJECT_IO_9097); + } + + @Test + public void test_ClusterUrls_NoLocalUrlToFilter() { + + when(environment.getProperty("server.port")).thenReturn("9197"); + List clusterUrls = restMessagingService.getHAClusterUrls(); + // Now with the environment returning port 9197 as the server.port we expect to see both + assertThat(clusterUrls).containsOnly("https://localhost:0", KLAW_PROJECT_IO_9097); + } + + @Test + public void testWithHttps_ClusterUrlsAsStringRestTemplateIsReturned() { + + RestTemplate restTemplate = restMessagingService.getRestTemplate(); + assertThat(restTemplate).isNotNull(); + } + + @Test + public void testWithHttp_ClusterUrlsAsStringRestTemplateIsReturned() { + ReflectionTestUtils.setField( + restMessagingService, + "clusterUrlsAsString", + "http://localhost:0,http://klaw-project.io:9097"); + RestTemplate restTemplate = restMessagingService.getRestTemplate(); + assertThat(restTemplate).isNotNull(); + } + + @Test + public void testRemoveEntryFromTheCache() { + ReflectionTestUtils.setField(restMessagingService, "rest", rest); + ReflectionTestUtils.setField( + restMessagingService, "clusterUrls", List.of(KLAW_PROJECT_IO_9097)); + + restMessagingService.sendRemove("environment", 101, 99000); + verify(rest, times(1)) + .exchange( + eq(KLAW_PROJECT_IO_9097 + "/cache/tenant/101/entityType/environment/id/99000"), + eq(HttpMethod.DELETE), + any(), + eq(Void.class)); + } + + @Test + public void testAddEntryFromTheCache() { + ReflectionTestUtils.setField(restMessagingService, "rest", rest); + ReflectionTestUtils.setField( + restMessagingService, "clusterUrls", List.of(KLAW_PROJECT_IO_9097)); + + Env env = new Env(); + env.setName("Dev1"); + env.setId("123"); + restMessagingService.sendUpdate("environment", 101, env); + verify(rest, times(1)) + .postForObject( + eq(KLAW_PROJECT_IO_9097 + "/cache/tenant/101/entityType/environment"), + any(), + eq(ApiResponse.class)); + } +} diff --git a/core/src/test/java/io/aiven/klaw/service/SchemaOverviewServiceTest.java b/core/src/test/java/io/aiven/klaw/service/SchemaOverviewServiceTest.java index b2c302679d..4b7412781a 100644 --- a/core/src/test/java/io/aiven/klaw/service/SchemaOverviewServiceTest.java +++ b/core/src/test/java/io/aiven/klaw/service/SchemaOverviewServiceTest.java @@ -419,6 +419,7 @@ private List createListOfEnvs(KafkaClustersType clusterType, int numberOfEn e.setTenantId(101); e.setClusterId(i); e.setType(clusterType.value); + when(manageDatabase.getEnv(eq(101), eq(i))).thenReturn(Optional.of(e)); envs.add(e); } @@ -436,10 +437,9 @@ private KwClusters createCluster(KafkaClustersType clusterType) { private void stubSchemaPromotionInfo( String testtopic, KafkaClustersType clusterType, int numberOfEnvs) throws Exception { - List listOfEnvs = createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs); - when(manageDatabase.getAllEnvList(101)).thenReturn(listOfEnvs); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs); + when(commonUtilsService.getTenantId(any())).thenReturn(101); - when(handleDbRequests.getAllSchemaRegEnvs(101)).thenReturn(listOfEnvs); when(manageDatabase.getClusters(eq(KafkaClustersType.SCHEMA_REGISTRY), eq(101))) .thenReturn(createClusterMap(numberOfEnvs)); when(clusterApiService.getAvroSchema(any(), any(), any(), eq(testtopic), eq(101))) diff --git a/core/src/test/java/io/aiven/klaw/service/TopicOverviewServiceTest.java b/core/src/test/java/io/aiven/klaw/service/TopicOverviewServiceTest.java index 3ff0264e35..724682e234 100644 --- a/core/src/test/java/io/aiven/klaw/service/TopicOverviewServiceTest.java +++ b/core/src/test/java/io/aiven/klaw/service/TopicOverviewServiceTest.java @@ -112,7 +112,8 @@ public void getAclsSyncFalse1() throws KlawException { when(commonUtilsService.getEnvsFromUserId(anyString())) .thenReturn(new HashSet<>(Collections.singletonList("1"))); when(manageDatabase.getKwPropertyValue(anyString(), anyInt())).thenReturn("true"); - when(manageDatabase.getKafkaEnvList(anyInt())).thenReturn(utilMethods.getEnvLists()); + when(manageDatabase.getEnv(anyInt(), anyInt())) + .thenReturn(Optional.of(utilMethods.getEnvLists().get(0))); when(handleDbRequests.getAllTeamsOfUsers(anyString(), anyInt())) .thenReturn(utilMethods.getTeams()); when(handleDbRequests.getSyncAcls(anyString(), anyString(), anyInt())) @@ -127,9 +128,8 @@ public void getAclsSyncFalse1() throws KlawException { when(manageDatabase.getClusters(any(KafkaClustersType.class), anyInt())) .thenReturn(kwClustersHashMap); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); when(commonUtilsService.getEnvProperty(eq(101), eq(REQUEST_TOPICS_OF_ENVS))).thenReturn("1"); mockTenantConfig(); TopicOverviewInfo topicOverviewInfo = @@ -158,7 +158,8 @@ public void getAclsSyncFalse2() { when(commonUtilsService.getEnvsFromUserId(anyString())) .thenReturn(new HashSet<>(Collections.singletonList("1"))); when(manageDatabase.getKwPropertyValue(anyString(), anyInt())).thenReturn("true"); - when(manageDatabase.getKafkaEnvList(anyInt())).thenReturn(utilMethods.getEnvLists()); + when(manageDatabase.getEnv(anyInt(), anyInt())) + .thenReturn(Optional.of(utilMethods.getEnvLists().get(0))); when(handleDbRequests.getAllTeamsOfUsers(anyString(), anyInt())) .thenReturn(utilMethods.getTeams()); when(handleDbRequests.getSyncAcls(anyString(), anyString(), anyInt())) @@ -171,8 +172,8 @@ public void getAclsSyncFalse2() { .thenReturn(kwClustersHashMap); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); + when(commonUtilsService.getEnvProperty(eq(101), eq(REQUEST_TOPICS_OF_ENVS))).thenReturn("1"); mockTenantConfig(); @@ -255,7 +256,8 @@ public void getAclsSyncFalseMaskedData() { when(commonUtilsService.getEnvsFromUserId(anyString())) .thenReturn(new HashSet<>(Collections.singletonList("1"))); when(manageDatabase.getKwPropertyValue(anyString(), anyInt())).thenReturn("true"); - when(manageDatabase.getKafkaEnvList(anyInt())).thenReturn(utilMethods.getEnvLists()); + when(manageDatabase.getEnv(anyInt(), anyInt())) + .thenReturn(Optional.of(utilMethods.getEnvLists().get(0))); when(handleDbRequests.getAllTeamsOfUsers(anyString(), anyInt())) .thenReturn(utilMethods.getTeams()); when(handleDbRequests.getSyncAcls(anyString(), anyString(), anyInt())) @@ -268,8 +270,8 @@ public void getAclsSyncFalseMaskedData() { .thenReturn(kwClustersHashMap); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); + when(commonUtilsService.getEnvProperty(eq(101), eq(REQUEST_TOPICS_OF_ENVS))).thenReturn("1"); mockTenantConfig(); List aclList = @@ -297,8 +299,8 @@ public void givenARequestWithAnOrderEnsureItIsCorrectlyUsed(AclGroupBy groupBy) .thenReturn(List.of(createTopic(TESTTOPIC))); when(handleDbRequests.getSyncAcls(anyString(), eq(TESTTOPIC), eq(101))) .thenReturn(createAcls(20)); - when(manageDatabase.getAllEnvList(eq(101))) - .thenReturn(createListOfEnvs(KafkaClustersType.KAFKA, 3)); + createListOfEnvs(KafkaClustersType.KAFKA, 3); + when(manageDatabase.getTeamNameFromTeamId(eq(101), anyInt())) .thenReturn("Octopus") .thenReturn("Town") @@ -345,8 +347,8 @@ public void getTopicOverview() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); + when(commonUtilsService.isCreateNewSchemaAllowed(eq("4"), eq(101))).thenReturn(true); TopicOverview topicOverview = @@ -415,8 +417,7 @@ public void getTopicOverviewWithNoAcls_OpenTopicRequests() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); TopicOverview topicOverview = topicOverviewService.getTopicOverview(TESTTOPIC, "1", AclGroupBy.NONE); @@ -475,8 +476,7 @@ public void getTopicOverviewWithNoAcls_OpenSchemaRequests() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); when(manageDatabase.getAssociatedSchemaEnvIdFromTopicId(eq("1"), eq(101))) .thenReturn(Optional.of("3")); @@ -541,8 +541,7 @@ public void getTopicOverviewWithNoAcls_OpenTopicACLSchemaRequests() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); TopicOverview topicOverview = topicOverviewService.getTopicOverview(TESTTOPIC, "1", AclGroupBy.NONE); @@ -607,8 +606,7 @@ public void getTopicOverviewWithNoAcls_hasSchema() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); TopicOverview topicOverview = topicOverviewService.getTopicOverview(TESTTOPIC, "1", AclGroupBy.NONE); @@ -721,8 +719,7 @@ public void getTopicOverviewWithNoAcls_OpenClaimRequest() { .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); when(kwClustersHashMap.get(anyInt())).thenReturn(kwClusters); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); when(manageDatabase.getAssociatedSchemaEnvIdFromTopicId(eq("1"), eq(101))) .thenReturn(Optional.of("3")); @@ -785,8 +782,7 @@ public void getTopicOverviewFromDifferentTeam_OnlyReturnClaimBoolean() { when(commonUtilsService.getEnvsFromUserId(anyString())) .thenReturn(new HashSet<>(Arrays.asList("1", "2", "3"))); when(commonUtilsService.getEnvProperty(eq(101), eq(ORDER_OF_TOPIC_ENVS))).thenReturn("1,2,3"); - when(manageDatabase.getAllEnvList(anyInt())) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, 5); when(commonUtilsService.getTopicsForTopicName(anyString(), anyInt())) .thenReturn(utilMethods.getTopicInMultipleEnvs("testtopic", TEAMID, 3)); @@ -865,7 +861,7 @@ private List createListOfEnvs(KafkaClustersType clusterType, int numberOfEn e.setTenantId(101); e.setClusterId(i); e.setType(clusterType.value); - envs.add(e); + when(manageDatabase.getEnv(eq(101), eq(i))).thenReturn(Optional.of(e)); } return envs; @@ -896,11 +892,9 @@ private TreeMap> createSchemaList() throws JsonProc private void stubSchemaPromotionInfo( String testtopic, KafkaClustersType clusterType, int numberOfEnvs) throws Exception { - when(manageDatabase.getAllEnvList(101)) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs); when(commonUtilsService.getTenantId(any())).thenReturn(101); - when(handleDbRequests.getAllSchemaRegEnvs(101)) - .thenReturn(createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs)); + createListOfEnvs(KafkaClustersType.SCHEMA_REGISTRY, numberOfEnvs); when(manageDatabase.getClusters(KafkaClustersType.SCHEMA_REGISTRY, 101)) .thenReturn(createClusterMap(numberOfEnvs)); when(clusterApiService.getAvroSchema(any(), any(), any(), eq(testtopic), eq(101))) diff --git a/core/src/test/resources/test-application-rdbms-ad-authorization.properties b/core/src/test/resources/test-application-rdbms-ad-authorization.properties index 79fe2dddbb..0135ac811f 100644 --- a/core/src/test/resources/test-application-rdbms-ad-authorization.properties +++ b/core/src/test/resources/test-application-rdbms-ad-authorization.properties @@ -112,7 +112,7 @@ klaw.reloadclusterstatus.interval=1800000 # ClusterApi access klaw.clusterapi.access.username=kwclusterapiuser - +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== # Monitoring klaw.monitoring.metrics.enable=false klaw.monitoring.metrics.collectinterval.ms=60000 @@ -135,7 +135,8 @@ klaw.saas.ssl.clusterapi.truststore=./client.truststore.jks klaw.saas.ssl.clusterapi.truststore.pwd=klaw klaw.saas.plaintext.aclcommand=kafka-acls --authorizer-properties bootstrap.server=host:port --add --allow-principal User:'*' --operation All --allow-host hostname_ip --cluster Cluster:kafka-cluster --topic "*" -klaw.uiapi.servers=https://localhost:9097 +#When local tests are run the server.port is configured to 0 +klaw.uiapi.servers=http://localhost:0 #google recaptcha settings google.recaptcha.sitekey=sitekey diff --git a/core/src/test/resources/test-application-rdbms-sso-ad.properties b/core/src/test/resources/test-application-rdbms-sso-ad.properties index f6f22e069b..505a50b760 100644 --- a/core/src/test/resources/test-application-rdbms-sso-ad.properties +++ b/core/src/test/resources/test-application-rdbms-sso-ad.properties @@ -112,7 +112,7 @@ klaw.reloadclusterstatus.interval=1800000 # ClusterApi access klaw.clusterapi.access.username=kwclusterapiuser - +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== # Monitoring klaw.monitoring.metrics.enable=false klaw.monitoring.metrics.collectinterval.ms=60000 @@ -135,7 +135,8 @@ klaw.saas.ssl.clusterapi.truststore=./client.truststore.jks klaw.saas.ssl.clusterapi.truststore.pwd=klaw klaw.saas.plaintext.aclcommand=kafka-acls --authorizer-properties bootstrap.server=host:port --add --allow-principal User:'*' --operation All --allow-host hostname_ip --cluster Cluster:kafka-cluster --topic "*" -klaw.uiapi.servers=https://localhost:9097 +#When local tests are run the server.port is configured to 0 +klaw.uiapi.servers=http://localhost:0 #google recaptcha settings google.recaptcha.sitekey=sitekey diff --git a/core/src/test/resources/test-application-rdbms-windows-ad.properties b/core/src/test/resources/test-application-rdbms-windows-ad.properties index f1d4cf106c..7c105ca5e1 100644 --- a/core/src/test/resources/test-application-rdbms-windows-ad.properties +++ b/core/src/test/resources/test-application-rdbms-windows-ad.properties @@ -72,7 +72,7 @@ klaw.enable.sso=false # Uncomment the below to establish connectivity between core and cluster apis. # Provide a base 64 encoded string. The same secret should be configured in Klaw Cluster Api. klaw.clusterapi.access.base64.secret= - +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== # In case of AD or Azure AD, configure an existing user from AD in the below config for username. Ex : superadmin@domain. # Leave it blank if this user is not required klaw.superadmin.default.username=superadmin @@ -87,7 +87,8 @@ klaw.saas.ssl.clusterapi.truststore.pwd=klaw klaw.saas.plaintext.aclcommand=kafka-acls --authorizer-properties bootstrap.server=host:port --add --allow-principal User:'*' --operation All --allow-host hostname_ip --cluster Cluster:kafka-cluster --topic "*" # comma separated instances of klaw when running in cluster -klaw.uiapi.servers=https://localhost:9097 +#When local tests are run the server.port is configured to 0 +klaw.uiapi.servers=http://localhost:0 # Enable new Klaw React based user interface # Make sure node, pnpm are installed. diff --git a/core/src/test/resources/test-application-rdbms.properties b/core/src/test/resources/test-application-rdbms.properties index 56c39a69a0..fd5e9fe22d 100644 --- a/core/src/test/resources/test-application-rdbms.properties +++ b/core/src/test/resources/test-application-rdbms.properties @@ -103,7 +103,7 @@ klaw.reloadclusterstatus.interval=1800000 # ClusterApi access klaw.clusterapi.access.username=kwclusterapiuser - +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== # Monitoring klaw.monitoring.metrics.enable=false klaw.monitoring.metrics.collectinterval.ms=60000 @@ -125,8 +125,8 @@ klaw.saas.ssl.clientcerts.location=./clientcerts klaw.saas.ssl.clusterapi.truststore=./client.truststore.jks klaw.saas.ssl.clusterapi.truststore.pwd=klaw klaw.saas.plaintext.aclcommand=kafka-acls --authorizer-properties bootstrap.server=host:port --add --allow-principal User:'*' --operation All --allow-host hostname_ip --cluster Cluster:kafka-cluster --topic "*" - -klaw.uiapi.servers=https://localhost:9097 +#When local tests are run the server.port is configured to 0 +klaw.uiapi.servers=http://localhost:0 #google recaptcha settings google.recaptcha.sitekey=sitekey diff --git a/core/src/test/resources/test-application-rdbms1.properties b/core/src/test/resources/test-application-rdbms1.properties index e39c196146..4f4ca53908 100644 --- a/core/src/test/resources/test-application-rdbms1.properties +++ b/core/src/test/resources/test-application-rdbms1.properties @@ -103,7 +103,7 @@ klaw.reloadclusterstatus.interval=1800000 # ClusterApi access klaw.clusterapi.access.username=kwclusterapiuser - +klaw.core.app2app.base64.secret=dGhpcyBpcyBhIHNlY3JldCB0byBhY2Nlc3MgY2x1c3RlcmFwaQ== # Monitoring klaw.monitoring.metrics.enable=false klaw.monitoring.metrics.collectinterval.ms=60000 @@ -125,8 +125,8 @@ klaw.saas.ssl.clientcerts.location=./clientcerts klaw.saas.ssl.clusterapi.truststore=./client.truststore.jks klaw.saas.ssl.clusterapi.truststore.pwd=klaw klaw.saas.plaintext.aclcommand=kafka-acls --authorizer-properties bootstrap.server=host:port --add --allow-principal User:'*' --operation All --allow-host hostname_ip --cluster Cluster:kafka-cluster --topic "*" - -klaw.uiapi.servers=https://localhost:9097 +#When local tests are run the server.port is configured to 0 +klaw.uiapi.servers=http://localhost:0 #google recaptcha settings google.recaptcha.sitekey=sitekey diff --git a/openapi.yaml b/openapi.yaml index 62d5bfb5e6..b799f8f686 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2076,6 +2076,50 @@ } } }, + "/cache/tenant/{tenantId}/entityType/environment" : { + "post" : { + "tags" : [ "cache-controller" ], + "operationId" : "addEnvToCache", + "parameters" : [ { + "name" : "tenantId", + "in" : "path", + "required" : true, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "name" : "Authorization", + "in" : "header", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Env" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, "/addTenantId" : { "post" : { "tags" : [ "envs-clusters-tenants-controller" ], @@ -4272,7 +4316,8 @@ "in" : "query", "required" : true, "schema" : { - "type" : "string" + "type" : "integer", + "format" : "int32" } } ], "responses" : { @@ -5776,6 +5821,48 @@ } } } + }, + "/cache/tenant/{tenantId}/entityType/environment/id/{id}" : { + "delete" : { + "tags" : [ "cache-controller" ], + "operationId" : "removeEnvFromCache", + "parameters" : [ { + "name" : "tenantId", + "in" : "path", + "required" : true, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "name" : "id", + "in" : "path", + "required" : true, + "schema" : { + "type" : "integer", + "format" : "int32" + } + }, { + "name" : "Authorization", + "in" : "header", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/ApiResponse" + } + } + } + } + } + } } }, "components" : { @@ -6793,87 +6880,52 @@ }, "required" : [ "connectorName", "envId", "includeOnlyFailedTasks" ] }, - "KwTenantModel" : { + "Env" : { "properties" : { - "tenantName" : { - "type" : "string", - "maxLength" : 25, - "minLength" : 12, - "pattern" : "^[a-zA-Z0-9]{3,}$" - }, - "tenantDesc" : { - "type" : "string", - "maxLength" : 25, - "minLength" : 6, - "pattern" : "^[a-zA-Z 0-9]{3,}$" + "id" : { + "type" : "string" }, "tenantId" : { "type" : "integer", "format" : "int32" }, - "licenseExpiryDate" : { + "name" : { "type" : "string" }, - "contactPerson" : { + "stretchCode" : { "type" : "string" }, - "inTrialPhase" : { - "type" : "boolean" + "clusterId" : { + "type" : "integer", + "format" : "int32" }, - "numberOfDays" : { + "type" : { "type" : "string" }, - "numberOfHours" : { + "otherParams" : { "type" : "string" }, - "orgName" : { + "envExists" : { "type" : "string" }, - "authorizedToDelete" : { - "type" : "boolean" - }, - "emailId" : { - "type" : "string" - }, - "activeTenant" : { - "type" : "boolean" - } - }, - "required" : [ "tenantDesc", "tenantName" ] - }, - "EnvModel" : { - "properties" : { - "name" : { + "envStatus" : { "type" : "string", - "maxLength" : 25, - "minLength" : 2, - "pattern" : "^[a-zA-Z0-9_.-]{3,}$" - }, - "type" : { - "type" : "string" - }, - "clusterId" : { - "type" : "integer", - "format" : "int32" + "enum" : [ "OFFLINE", "ONLINE", "NOT_KNOWN" ] }, - "otherParams" : { - "type" : "string" + "envStatusTime" : { + "type" : "string", + "format" : "date-time" }, - "id" : { + "envStatusTimeString" : { "type" : "string" }, "associatedEnv" : { "$ref" : "#/components/schemas/EnvTag" }, - "tenantId" : { - "type" : "integer", - "format" : "int32" - }, "params" : { "$ref" : "#/components/schemas/EnvParams" } - }, - "required" : [ "clusterId", "name", "type" ] + } }, "EnvParams" : { "properties" : { @@ -6934,6 +6986,88 @@ } } }, + "KwTenantModel" : { + "properties" : { + "tenantName" : { + "type" : "string", + "maxLength" : 25, + "minLength" : 12, + "pattern" : "^[a-zA-Z0-9]{3,}$" + }, + "tenantDesc" : { + "type" : "string", + "maxLength" : 25, + "minLength" : 6, + "pattern" : "^[a-zA-Z 0-9]{3,}$" + }, + "tenantId" : { + "type" : "integer", + "format" : "int32" + }, + "licenseExpiryDate" : { + "type" : "string" + }, + "contactPerson" : { + "type" : "string" + }, + "inTrialPhase" : { + "type" : "boolean" + }, + "numberOfDays" : { + "type" : "string" + }, + "numberOfHours" : { + "type" : "string" + }, + "orgName" : { + "type" : "string" + }, + "authorizedToDelete" : { + "type" : "boolean" + }, + "emailId" : { + "type" : "string" + }, + "activeTenant" : { + "type" : "boolean" + } + }, + "required" : [ "tenantDesc", "tenantName" ] + }, + "EnvModel" : { + "properties" : { + "name" : { + "type" : "string", + "maxLength" : 25, + "minLength" : 2, + "pattern" : "^[a-zA-Z0-9_.-]{3,}$" + }, + "type" : { + "type" : "string" + }, + "clusterId" : { + "type" : "integer", + "format" : "int32" + }, + "otherParams" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "associatedEnv" : { + "$ref" : "#/components/schemas/EnvTag" + }, + "tenantId" : { + "type" : "integer", + "format" : "int32" + }, + "params" : { + "$ref" : "#/components/schemas/EnvParams" + } + }, + "required" : [ "clusterId", "name", "type" ] + }, "KwClustersModel" : { "properties" : { "clusterId" : { @@ -7506,9 +7640,16 @@ "envStatus" : { "type" : "string", "enum" : [ "OFFLINE", "ONLINE", "NOT_KNOWN" ] + }, + "envStatusTime" : { + "type" : "string", + "format" : "date-time" + }, + "envStatusTimeString" : { + "type" : "string" } }, - "required" : [ "envStatus", "result" ] + "required" : [ "envStatus", "envStatusTime", "envStatusTimeString", "result" ] }, "TopicsCountPerEnv" : { "properties" : { @@ -7908,13 +8049,13 @@ }, "Scales" : { "properties" : { - "xaxes" : { + "yaxes" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/YAx" } }, - "yaxes" : { + "xaxes" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/YAx" @@ -8252,6 +8393,13 @@ "type" : "string", "enum" : [ "OFFLINE", "ONLINE", "NOT_KNOWN" ] }, + "envStatusTime" : { + "type" : "string", + "format" : "date-time" + }, + "envStatusTimeString" : { + "type" : "string" + }, "otherParams" : { "type" : "string" }, @@ -8286,7 +8434,7 @@ "writeOnly" : true } }, - "required" : [ "allPageNos", "clusterId", "clusterName", "currentPage", "envStatus", "id", "name", "otherParams", "params", "showDeleteEnv", "tenantId", "tenantName", "totalNoPages", "totalRecs", "type" ] + "required" : [ "allPageNos", "clusterId", "clusterName", "currentPage", "envStatus", "envStatusTime", "envStatusTimeString", "id", "name", "otherParams", "params", "showDeleteEnv", "tenantId", "tenantName", "totalNoPages", "totalRecs", "type" ] }, "AclInfo" : { "properties" : {