diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/TenantConfigurationCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/TenantConfigurationCE.java index 065381e193..8e71b69858 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/TenantConfigurationCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/TenantConfigurationCE.java @@ -47,6 +47,10 @@ public class TenantConfigurationCE { // 2. Because of grandfathering via cron where tenant level feature flags are fetched Map featuresWithPendingMigration; + // This variable is used to indicate if the server needs to be restarted after the migration based on feature flags + // is complete. + Boolean isRestartRequired; + public void addThirdPartyAuth(String auth) { if (thirdPartyAuths == null) { thirdPartyAuths = new ArrayList<>(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelper.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelper.java index a2041e07f3..d47b6b07bc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelper.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelper.java @@ -1,254 +1,5 @@ package com.appsmith.server.helpers; -import com.appsmith.server.constants.FeatureMigrationType; -import com.appsmith.server.domains.Tenant; -import com.appsmith.server.domains.TenantConfiguration; -import com.appsmith.server.featureflags.CachedFeatures; -import com.appsmith.server.featureflags.FeatureFlagEnum; -import com.appsmith.server.services.CacheableFeatureFlagHelper; -import com.appsmith.server.solutions.ce.ScheduledTaskCEImpl; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; +import com.appsmith.server.helpers.ce.FeatureFlagMigrationHelperCE; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.Map; - -import static com.appsmith.server.constants.FeatureMigrationType.DISABLE; -import static com.appsmith.server.constants.FeatureMigrationType.ENABLE; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; - -@Component -@RequiredArgsConstructor -@Slf4j -public class FeatureFlagMigrationHelper { - - private final CacheableFeatureFlagHelper cacheableFeatureFlagHelper; - - /** - * To avoid race condition keep the refresh rate lower than cron execution interval {@link ScheduledTaskCEImpl} - * to update the tenant level feature flags - */ - private static final long TENANT_FEATURES_CACHE_TIME_MIN = 115; - - public Mono> getUpdatedFlagsWithPendingMigration(Tenant defaultTenant) { - return getUpdatedFlagsWithPendingMigration(defaultTenant, FALSE); - } - - /** - * Method to get the updated feature flags with pending migrations. This method finds and registers the flags for - * migration by comparing the diffs between the feature flags stored in cache and the latest one pulled from CS - * @param tenant Tenant for which the feature flags need to be updated - * @param forceUpdate Flag to force update the tenant level feature flags - * @return Map of feature flags with pending migrations - */ - public Mono> getUpdatedFlagsWithPendingMigration( - Tenant tenant, boolean forceUpdate) { - - /* - * 1. Fetch current/saved feature flags from cache - * 2. Force update the tenant flags keeping existing flags as fallback in case the API call to fetch the flags fails for some reason - * 3. Get the diff and update the flags with pending migrations to be used to run migrations selectively - */ - return cacheableFeatureFlagHelper - .fetchCachedTenantFeatures(tenant.getId()) - .zipWhen(existingCachedFlags -> { - if (existingCachedFlags.getRefreshedAt().until(Instant.now(), ChronoUnit.MINUTES) - < TENANT_FEATURES_CACHE_TIME_MIN - && !forceUpdate) { - return Mono.just(existingCachedFlags); - } - return this.refreshTenantFeatures(tenant, existingCachedFlags); - }) - .map(tuple2 -> { - CachedFeatures existingCachedFlags = tuple2.getT1(); - CachedFeatures latestFlags = tuple2.getT2(); - return this.getUpdatedFlagsWithPendingMigration(tenant, latestFlags, existingCachedFlags); - }); - } - - /** - * Method to force update the tenant level feature flags. This will be utilised in scenarios where we don't want - * to wait for the flags to get updated for cron scheduled time - * - * @param tenant tenant for which the feature flags need to be updated - * @return Cached features - */ - private Mono refreshTenantFeatures(Tenant tenant, CachedFeatures existingCachedFeatures) { - /* - 1. Force update the flag - a. Evict the cache - b. Fetch and save latest flags from CS - 2. In case the tenant is unable to fetch the latest flags save the existing flags from step 1 to cache (fallback) - */ - String tenantId = tenant.getId(); - return cacheableFeatureFlagHelper - .evictCachedTenantFeatures(tenantId) - .then(cacheableFeatureFlagHelper.fetchCachedTenantFeatures(tenantId)) - .flatMap(features -> { - if (CollectionUtils.isNullOrEmpty(features.getFeatures())) { - // In case the retrieval of the latest flags from CS encounters an error, the previous flags - // will serve as a fallback value. - return cacheableFeatureFlagHelper.updateCachedTenantFeatures(tenantId, existingCachedFeatures); - } - return Mono.just(features); - }); - } - - /** - * Method to check the diffs between the existing feature flags and the latest flags pulled from CS. If there are - * any diffs save the flags with required migration types: - * Flag transitions: - * 1. false -> true : Migration to enable the feature flag - * 2. true -> false : Migration to disable the feature flag - * 3. There is a scenario when the migrations will be blocked on user input and may end up in a case where we just - * have to remove the entry as migration it's no longer needed: - * Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered - * Step 2: License expires which results in feature getting disabled so migration entry gets registered with - * disable type (This will happen via cron to check the license status) - * Step 3: As the migration will be blocked by the user input for downgrade migration, DB state will be - * maintained - * Step 4: User adds the valid key or renews the subscription again which results in enabling the feature and - * ends up in nullifying the effect for step 2 - * - * @param tenant Tenant for which the feature flag migrations stats needs to be stored - * @param latestFlags Latest flags pulled in from CS - * @param existingCachedFlags Flags which are already stored in cache - * @return updated tenant with the required flags with pending migrations - */ - private Map getUpdatedFlagsWithPendingMigration( - Tenant tenant, CachedFeatures latestFlags, CachedFeatures existingCachedFlags) { - - // 1. Check if there are any diffs for the feature flags - // 2. Update the flags for pending migration within provided tenant object - Map featureDiffsWithMigrationType = new HashMap<>(); - Map existingFeatureMap = existingCachedFlags.getFeatures(); - latestFlags.getFeatures().forEach((key, value) -> { - if (value != null && !value.equals(existingFeatureMap.get(key))) { - try { - featureDiffsWithMigrationType.put( - FeatureFlagEnum.valueOf(key), Boolean.TRUE.equals(value) ? ENABLE : DISABLE); - } catch (Exception e) { - // Ignore IllegalArgumentException as all the feature flags are not added on - // server side - } - } - }); - return getUpdatedFlagsWithPendingMigration(featureDiffsWithMigrationType, tenant); - } - - private Map getUpdatedFlagsWithPendingMigration( - Map latestFeatureDiffsWithMigrationType, Tenant dbTenant) { - - Map featuresWithPendingMigrationDB = - dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration() == null - ? new HashMap<>() - : dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration(); - - Map updatedFlagsForMigrations = - new HashMap<>(featuresWithPendingMigrationDB); - - // We should expect the following state after the latest run: - // featuresWithPendingMigrationDB => {feature1 : enable, feature2 : disable} - // latestFeatureDiffsWithMigrationType => {feature1 : enable, feature2 : enable, feature3 : disable} - // updatedFlagsForMigrations => {feature1 : enable, feature3 : disable} - - updatedFlagsForMigrations.forEach((featureFlagEnum, featureMigrationType) -> { - if (latestFeatureDiffsWithMigrationType.containsKey(featureFlagEnum) - && !featureMigrationType.equals(latestFeatureDiffsWithMigrationType.get(featureFlagEnum))) { - /* - Scenario when the migrations will be blocked on user input and may end up in a case where we just have - to remove the entry as migration it's no longer needed: - Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered - Step 2: License expires which results in feature getting disabled so migration entry gets registered - with disable type (This will happen via cron to check the license status) - Step 3: As the migration will be blocked by the user input for downgrade migration, DB state will be - maintained - Step 4: User adds the valid key or renews the subscription again which results in enabling the - feature and ends up in nullifying the effect for step 2 - */ - updatedFlagsForMigrations.remove(featureFlagEnum); - latestFeatureDiffsWithMigrationType.remove(featureFlagEnum); - } - }); - // Add the latest flags which were not part of earlier check. - updatedFlagsForMigrations.putAll(latestFeatureDiffsWithMigrationType); - return updatedFlagsForMigrations; - } - - /** - * Method to check and execute if the migrations are required for the provided feature flag. - * @param tenant Tenant for which the migrations need to be executed - * @param featureFlagEnum Feature flag for which the migrations need to be executed - * @return Boolean indicating if the migrations is successfully executed or not - */ - public Mono checkAndExecuteMigrationsForFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum) { - - TenantConfiguration tenantConfiguration = tenant.getTenantConfiguration(); - if (featureFlagEnum == null - || tenantConfiguration == null - || CollectionUtils.isNullOrEmpty(tenantConfiguration.getFeaturesWithPendingMigration())) { - return Mono.just(TRUE); - } - return isMigrationRequired(tenant, featureFlagEnum).flatMap(isMigrationRequired -> { - if (FALSE.equals(isMigrationRequired)) { - return Mono.just(TRUE); - } - return this.executeMigrationsBasedOnFeatureFlag(tenantConfiguration, featureFlagEnum); - }); - } - - /** - * Method to check if the migrations are required for the provided feature flag. - * @param tenant Tenant for which the migrations need to be executed - * @param featureFlagEnum Feature flag for which the migrations need to be executed - * @return Boolean indicating if the migrations is required or not - */ - private Mono isMigrationRequired(Tenant tenant, FeatureFlagEnum featureFlagEnum) { - Map featureMigrationTypeMap = - tenant.getTenantConfiguration().getFeaturesWithPendingMigration(); - if (CollectionUtils.isNullOrEmpty(featureMigrationTypeMap)) { - return Mono.just(FALSE); - } - return cacheableFeatureFlagHelper - .fetchCachedTenantFeatures(tenant.getId()) - .map(cachedFeatures -> { - Map featureFlags = cachedFeatures.getFeatures(); - if (featureFlags.containsKey(featureFlagEnum.name())) { - return (TRUE.equals(featureFlags.get(featureFlagEnum.name())) - && FeatureMigrationType.ENABLE.equals( - featureMigrationTypeMap.get(featureFlagEnum))) - || (FALSE.equals(featureFlags.get(featureFlagEnum.name())) - && FeatureMigrationType.DISABLE.equals( - featureMigrationTypeMap.get(featureFlagEnum))); - } - return FALSE; - }); - } - - /** - * Method to execute the migrations for the given feature flag. - * @param tenantConfiguration Tenant configuration for which the migrations need to be executed - * @param featureFlagEnum Feature flag for which the migrations need to be executed - * @return Boolean indicating if the migrations is successfully executed or not - */ - private Mono executeMigrationsBasedOnFeatureFlag( - TenantConfiguration tenantConfiguration, FeatureFlagEnum featureFlagEnum) { - // TODO implement migrations as per the supported features in license plan - Map featuresWithPendingMigration = - tenantConfiguration.getFeaturesWithPendingMigration(); - if (CollectionUtils.isNullOrEmpty(featuresWithPendingMigration) - || !featuresWithPendingMigration.containsKey(featureFlagEnum)) { - return Mono.just(TRUE); - } - log.debug( - "Running the migration for flag {} with migration type {}", - featureFlagEnum.name(), - featuresWithPendingMigration.get(featureFlagEnum)); - return Mono.just(TRUE); - } -} +public interface FeatureFlagMigrationHelper extends FeatureFlagMigrationHelperCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperImpl.java new file mode 100644 index 0000000000..79bdfee8c6 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperImpl.java @@ -0,0 +1,13 @@ +package com.appsmith.server.helpers; + +import com.appsmith.server.helpers.ce.FeatureFlagMigrationHelperCEImpl; +import com.appsmith.server.services.CacheableFeatureFlagHelper; +import org.springframework.stereotype.Component; + +@Component +public class FeatureFlagMigrationHelperImpl extends FeatureFlagMigrationHelperCEImpl + implements FeatureFlagMigrationHelper { + public FeatureFlagMigrationHelperImpl(CacheableFeatureFlagHelper cacheableFeatureFlagHelper) { + super(cacheableFeatureFlagHelper); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCE.java new file mode 100644 index 0000000000..1e4489d0fa --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCE.java @@ -0,0 +1,20 @@ +package com.appsmith.server.helpers.ce; + +import com.appsmith.server.constants.FeatureMigrationType; +import com.appsmith.server.domains.Tenant; +import com.appsmith.server.featureflags.FeatureFlagEnum; +import reactor.core.publisher.Mono; + +import java.util.Map; + +public interface FeatureFlagMigrationHelperCE { + + Mono> getUpdatedFlagsWithPendingMigration(Tenant defaultTenant); + + Mono> getUpdatedFlagsWithPendingMigration( + Tenant tenant, boolean forceUpdate); + + Mono checkAndExecuteMigrationsForFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum); + + Mono executeMigrationsBasedOnFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCEImpl.java new file mode 100644 index 0000000000..3980530087 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/FeatureFlagMigrationHelperCEImpl.java @@ -0,0 +1,255 @@ +package com.appsmith.server.helpers.ce; + +import com.appsmith.server.constants.FeatureMigrationType; +import com.appsmith.server.domains.Tenant; +import com.appsmith.server.domains.TenantConfiguration; +import com.appsmith.server.featureflags.CachedFeatures; +import com.appsmith.server.featureflags.FeatureFlagEnum; +import com.appsmith.server.helpers.CollectionUtils; +import com.appsmith.server.services.CacheableFeatureFlagHelper; +import com.appsmith.server.solutions.ce.ScheduledTaskCEImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +import static com.appsmith.server.constants.FeatureMigrationType.DISABLE; +import static com.appsmith.server.constants.FeatureMigrationType.ENABLE; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +@RequiredArgsConstructor +@Slf4j +public class FeatureFlagMigrationHelperCEImpl implements FeatureFlagMigrationHelperCE { + + private final CacheableFeatureFlagHelper cacheableFeatureFlagHelper; + + /** + * To avoid race condition keep the refresh rate lower than cron execution interval {@link ScheduledTaskCEImpl} + * to update the tenant level feature flags + */ + private static final long TENANT_FEATURES_CACHE_TIME_MIN = 115; + + @Override + public Mono> getUpdatedFlagsWithPendingMigration(Tenant defaultTenant) { + return getUpdatedFlagsWithPendingMigration(defaultTenant, FALSE); + } + + /** + * Method to get the updated feature flags with pending migrations. This method finds and registers the flags for + * migration by comparing the diffs between the feature flags stored in cache and the latest one pulled from CS + * @param tenant Tenant for which the feature flags need to be updated + * @param forceUpdate Flag to force update the tenant level feature flags + * @return Map of feature flags with pending migrations + */ + @Override + public Mono> getUpdatedFlagsWithPendingMigration( + Tenant tenant, boolean forceUpdate) { + + /* + * 1. Fetch current/saved feature flags from cache + * 2. Force update the tenant flags keeping existing flags as fallback in case the API call to fetch the flags fails for some reason + * 3. Get the diff and update the flags with pending migrations to be used to run migrations selectively + */ + return cacheableFeatureFlagHelper + .fetchCachedTenantFeatures(tenant.getId()) + .zipWhen(existingCachedFlags -> { + if (existingCachedFlags.getRefreshedAt().until(Instant.now(), ChronoUnit.MINUTES) + < TENANT_FEATURES_CACHE_TIME_MIN + && !forceUpdate) { + return Mono.just(existingCachedFlags); + } + return this.refreshTenantFeatures(tenant, existingCachedFlags); + }) + .map(tuple2 -> { + CachedFeatures existingCachedFlags = tuple2.getT1(); + CachedFeatures latestFlags = tuple2.getT2(); + return this.getUpdatedFlagsWithPendingMigration(tenant, latestFlags, existingCachedFlags); + }); + } + + /** + * Method to force update the tenant level feature flags. This will be utilised in scenarios where we don't want + * to wait for the flags to get updated for cron scheduled time + * + * @param tenant tenant for which the feature flags need to be updated + * @return Cached features + */ + private Mono refreshTenantFeatures(Tenant tenant, CachedFeatures existingCachedFeatures) { + /* + 1. Force update the flag + a. Evict the cache + b. Fetch and save latest flags from CS + 2. In case the tenant is unable to fetch the latest flags save the existing flags from step 1 to cache (fallback) + */ + String tenantId = tenant.getId(); + return cacheableFeatureFlagHelper + .evictCachedTenantFeatures(tenantId) + .then(cacheableFeatureFlagHelper.fetchCachedTenantFeatures(tenantId)) + .flatMap(features -> { + if (CollectionUtils.isNullOrEmpty(features.getFeatures())) { + // In case the retrieval of the latest flags from CS encounters an error, the previous flags + // will serve as a fallback value. + return cacheableFeatureFlagHelper.updateCachedTenantFeatures(tenantId, existingCachedFeatures); + } + return Mono.just(features); + }); + } + + /** + * Method to check the diffs between the existing feature flags and the latest flags pulled from CS. If there are + * any diffs save the flags with required migration types: + * Flag transitions: + * 1. false -> true : Migration to enable the feature flag + * 2. true -> false : Migration to disable the feature flag + * 3. There is a scenario when the migrations will be blocked on user input and may end up in a case where we just + * have to remove the entry as migration it's no longer needed: + * Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered + * Step 2: License expires which results in feature getting disabled so migration entry gets registered with + * disable type (This will happen via cron to check the license status) + * Step 3: As the migration will be blocked by the user input for downgrade migration, DB state will be + * maintained + * Step 4: User adds the valid key or renews the subscription again which results in enabling the feature and + * ends up in nullifying the effect for step 2 + * + * @param tenant Tenant for which the feature flag migrations stats needs to be stored + * @param latestFlags Latest flags pulled in from CS + * @param existingCachedFlags Flags which are already stored in cache + * @return updated tenant with the required flags with pending migrations + */ + private Map getUpdatedFlagsWithPendingMigration( + Tenant tenant, CachedFeatures latestFlags, CachedFeatures existingCachedFlags) { + + // 1. Check if there are any diffs for the feature flags + // 2. Update the flags for pending migration within provided tenant object + Map featureDiffsWithMigrationType = new HashMap<>(); + Map existingFeatureMap = existingCachedFlags.getFeatures(); + latestFlags.getFeatures().forEach((key, value) -> { + if (value != null && !value.equals(existingFeatureMap.get(key))) { + try { + featureDiffsWithMigrationType.put( + FeatureFlagEnum.valueOf(key), Boolean.TRUE.equals(value) ? ENABLE : DISABLE); + } catch (Exception e) { + // Ignore IllegalArgumentException as all the feature flags are not added on + // server side + } + } + }); + return getUpdatedFlagsWithPendingMigration(featureDiffsWithMigrationType, tenant); + } + + private Map getUpdatedFlagsWithPendingMigration( + Map latestFeatureDiffsWithMigrationType, Tenant dbTenant) { + + Map featuresWithPendingMigrationDB = + dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration() == null + ? new HashMap<>() + : dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration(); + + Map updatedFlagsForMigrations = + new HashMap<>(featuresWithPendingMigrationDB); + + // We should expect the following state after the latest run: + // featuresWithPendingMigrationDB => {feature1 : enable, feature2 : disable} + // latestFeatureDiffsWithMigrationType => {feature1 : enable, feature2 : enable, feature3 : disable} + // updatedFlagsForMigrations => {feature1 : enable, feature3 : disable} + + updatedFlagsForMigrations.forEach((featureFlagEnum, featureMigrationType) -> { + if (latestFeatureDiffsWithMigrationType.containsKey(featureFlagEnum) + && !featureMigrationType.equals(latestFeatureDiffsWithMigrationType.get(featureFlagEnum))) { + /* + Scenario when the migrations will be blocked on user input and may end up in a case where we just have + to remove the entry as migration it's no longer needed: + Step 1: Feature gets enabled by adding a valid licence and enable migration gets registered + Step 2: License expires which results in feature getting disabled so migration entry gets registered + with disable type (This will happen via cron to check the license status) + Step 3: As the migration will be blocked by the user input for downgrade migration, DB state will be + maintained + Step 4: User adds the valid key or renews the subscription again which results in enabling the + feature and ends up in nullifying the effect for step 2 + */ + updatedFlagsForMigrations.remove(featureFlagEnum); + latestFeatureDiffsWithMigrationType.remove(featureFlagEnum); + } + }); + // Add the latest flags which were not part of earlier check. + updatedFlagsForMigrations.putAll(latestFeatureDiffsWithMigrationType); + return updatedFlagsForMigrations; + } + + /** + * Method to check and execute if the migrations are required for the provided feature flag. + * @param tenant Tenant for which the migrations need to be executed + * @param featureFlagEnum Feature flag for which the migrations need to be executed + * @return Boolean indicating if the migrations is successfully executed or not + */ + public Mono checkAndExecuteMigrationsForFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum) { + + TenantConfiguration tenantConfiguration = tenant.getTenantConfiguration(); + if (featureFlagEnum == null + || tenantConfiguration == null + || CollectionUtils.isNullOrEmpty(tenantConfiguration.getFeaturesWithPendingMigration())) { + return Mono.just(TRUE); + } + return isMigrationRequired(tenant, featureFlagEnum).flatMap(isMigrationRequired -> { + if (FALSE.equals(isMigrationRequired)) { + return Mono.just(TRUE); + } + + Map featuresWithPendingMigration = + tenantConfiguration.getFeaturesWithPendingMigration(); + if (CollectionUtils.isNullOrEmpty(featuresWithPendingMigration) + || !featuresWithPendingMigration.containsKey(featureFlagEnum)) { + return Mono.just(TRUE); + } + log.debug( + "Running the migration for flag {} with migration type {}", + featureFlagEnum.name(), + featuresWithPendingMigration.get(featureFlagEnum)); + return this.executeMigrationsBasedOnFeatureFlag(tenant, featureFlagEnum); + }); + } + + /** + * Method to check if the migrations are required for the provided feature flag. + * @param tenant Tenant for which the migrations need to be executed + * @param featureFlagEnum Feature flag for which the migrations need to be executed + * @return Boolean indicating if the migrations is required or not + */ + private Mono isMigrationRequired(Tenant tenant, FeatureFlagEnum featureFlagEnum) { + Map featureMigrationTypeMap = + tenant.getTenantConfiguration().getFeaturesWithPendingMigration(); + if (CollectionUtils.isNullOrEmpty(featureMigrationTypeMap)) { + return Mono.just(FALSE); + } + return cacheableFeatureFlagHelper + .fetchCachedTenantFeatures(tenant.getId()) + .map(cachedFeatures -> { + Map featureFlags = cachedFeatures.getFeatures(); + if (featureFlags.containsKey(featureFlagEnum.name())) { + return (TRUE.equals(featureFlags.get(featureFlagEnum.name())) + && FeatureMigrationType.ENABLE.equals( + featureMigrationTypeMap.get(featureFlagEnum))) + || (FALSE.equals(featureFlags.get(featureFlagEnum.name())) + && FeatureMigrationType.DISABLE.equals( + featureMigrationTypeMap.get(featureFlagEnum))); + } + return FALSE; + }); + } + + /** + * Method to execute the migrations for the given feature flag. + * @param tenant Tenant for which the migrations need to be executed + * @param featureFlagEnum Feature flag for which the migrations need to be executed + * @return Boolean indicating if the migrations is successfully executed or not + */ + @Override + public Mono executeMigrationsBasedOnFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum) { + return Mono.just(TRUE); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java index 136f6a26a0..55a003e2c2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCE.java @@ -2,7 +2,9 @@ package com.appsmith.server.services.ce; import com.appsmith.server.domains.User; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; import java.util.List; @@ -17,4 +19,6 @@ public interface SessionUserServiceCE { Mono> getSessionKeysByUserEmail(String email); Mono deleteSessionsByKeys(List keys); + + Flux> getSessionKeysWithUserSessions(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java index 97c44f98b7..082a22c507 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/SessionUserServiceCEImpl.java @@ -16,6 +16,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -78,9 +79,28 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { .then(); } + /** + * This method returns a list of session keys, for the given user email. + * @param email The email of the user whose sessions keys should be fetched. + * @return A Mono of list of session keys. + */ @Override public Mono> getSessionKeysByUserEmail(String email) { // This pattern string comes from calling `ReactiveRedisSessionRepository.getSessionKey("*")` private method. + return getSessionKeysWithUserSessions() + // Now we have tuples of session keys, and the corresponding user objects. + // Filter the ones we need to clear out. + .filter(tuple -> + StringUtils.equalsIgnoreCase(email, tuple.getT2().getEmail())) + .map(Tuple2::getT1) + .collectList(); + } + + /** + * This method returns a Flux of tuples, where the first element is the session key, and the second element is the + * corresponding User object. + */ + public Flux> getSessionKeysWithUserSessions() { return redisOperations .keys(SPRING_SESSION_PATTERN) .flatMap(key -> Mono.zip( @@ -96,13 +116,7 @@ public class SessionUserServiceCEImpl implements SessionUserServiceCE { .map(e -> (User) ((SecurityContext) e.getValue()) .getAuthentication() .getPrincipal()) - .next())) - // Now we have tuples of session keys, and the corresponding user objects. - // Filter the ones we need to clear out. - .filter(tuple -> - StringUtils.equalsIgnoreCase(email, tuple.getT2().getEmail())) - .map(Tuple2::getT1) - .collectList(); + .next())); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCE.java index c46570d670..8708b98d13 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCE.java @@ -23,4 +23,8 @@ public interface TenantServiceCE extends CrudService { Mono save(Tenant tenant); Mono checkAndExecuteMigrationsForTenantFeatureFlags(Tenant tenant); + + Mono retrieveById(String id); + + Mono restartTenant(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCEImpl.java index eea73eded1..0609d0b0a1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/TenantServiceCEImpl.java @@ -18,6 +18,7 @@ import com.appsmith.server.services.BaseService; import com.appsmith.server.services.ConfigService; import com.appsmith.server.solutions.EnvManager; import jakarta.validation.Validator; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -30,6 +31,7 @@ import java.util.Map; import static com.appsmith.server.acl.AclPermission.MANAGE_TENANT; import static java.lang.Boolean.TRUE; +@Slf4j public class TenantServiceCEImpl extends BaseService implements TenantServiceCE { private String tenantId = null; @@ -233,6 +235,35 @@ public class TenantServiceCEImpl extends BaseService retrieveById(String id) { + if (!StringUtils.hasLength(id)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID)); + } + return repository.retrieveById(id); + } + + /** + * This function checks if the tenant needs to be restarted and restarts after the feature flag migrations are + * executed. + * + * @return + */ + @Override + public Mono restartTenant() { + // Avoid dependency on user context as this method will be called internally by the server + Mono defaultTenantMono = this.getDefaultTenantId().flatMap(this::retrieveById); + return defaultTenantMono.flatMap(updatedTenant -> { + if (TRUE.equals(updatedTenant.getTenantConfiguration().getIsRestartRequired())) { + log.debug("Triggering tenant restart after the feature flag migrations are executed"); + TenantConfiguration tenantConfiguration = updatedTenant.getTenantConfiguration(); + tenantConfiguration.setIsRestartRequired(false); + return this.update(updatedTenant.getId(), updatedTenant).then(envManager.restartWithoutAclCheck()); + } + return Mono.empty(); + }); + } + private boolean isMigrationRequired(Tenant tenant) { return tenant.getTenantConfiguration() != null && (!CollectionUtils.isNullOrEmpty( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCE.java index 6fdf4275fc..dae179bca3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCE.java @@ -16,6 +16,8 @@ public interface EnvManagerCE { Mono applyChanges(Map changes, String originHeader); + Mono> applyChangesToEnvFileWithoutAclCheck(Map changes); + Mono applyChangesFromMultipartFormData(MultiValueMap formData, String originHeader); void setAnalyticsEventAction( @@ -25,6 +27,8 @@ public interface EnvManagerCE { Map parseToMap(String content); + Mono> getAllWithoutAclCheck(); + Mono> getAll(); Mono> getAllNonEmpty(); @@ -33,6 +37,8 @@ public interface EnvManagerCE { Mono restart(); + Mono restartWithoutAclCheck(); + Mono sendTestEmail(TestEmailConfigRequestDTO requestDTO); Mono download(ServerWebExchange exchange); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java index aaf8bde2aa..afe128147f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/EnvManagerCEImpl.java @@ -13,6 +13,7 @@ import com.appsmith.server.dtos.TestEmailConfigRequestDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.CollectionUtils; +import com.appsmith.server.helpers.FeatureFlagMigrationHelper; import com.appsmith.server.helpers.FileUtils; import com.appsmith.server.helpers.TextUtils; import com.appsmith.server.helpers.UserUtils; @@ -269,6 +270,8 @@ public class EnvManagerCEImpl implements EnvManagerCE { return valueBuilder.toString(); } + // Expect user object to be null when this method is getting called to run the tenant specific migrations without + // user context private Mono validateChanges(User user, Map changes) { if (changes.containsKey(APPSMITH_ADMIN_EMAILS.name())) { String emailCsv = StringUtils.trimAllWhitespace(changes.get(APPSMITH_ADMIN_EMAILS.name())); @@ -278,7 +281,7 @@ public class EnvManagerCEImpl implements EnvManagerCE { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "Admin Emails")); } else { // make sure user is not removing own email Set adminEmails = TextUtils.csvToSet(emailCsv); - if (!adminEmails.contains(user.getEmail())) { // user can not remove own email address + if (user != null && !adminEmails.contains(user.getEmail())) { // user can not remove own email address return Mono.error(new AppsmithException( AppsmithError.GENERIC_BAD_REQUEST, "Removing own email from Admin Email is not allowed")); } @@ -347,43 +350,14 @@ public class EnvManagerCEImpl implements EnvManagerCE { // configuration return verifyCurrentUserIsSuper() .flatMap(user -> validateChanges(user, changes).thenReturn(user)) - .flatMap(user -> { - // Write the changes to the env file. - final String originalContent; - final Path envFilePath = Path.of(commonConfig.getEnvFilePath()); - - try { - originalContent = Files.readString(envFilePath); - } catch (IOException e) { - log.error("Unable to read env file " + envFilePath, e); - return Mono.error(e); - } - Map originalVariables = parseToMap(originalContent); - - final Map envFileChanges = new HashMap<>(changes); - final Set tenantConfigurationKeys = allowedTenantConfiguration(); - for (final String key : changes.keySet()) { - if (tenantConfigurationKeys.contains(key)) { - envFileChanges.remove(key); - } - } - final List changedContent = transformEnvContent(originalContent, envFileChanges); - - try { - Files.write(envFilePath, changedContent); - } catch (IOException e) { - log.error("Unable to write to env file " + envFilePath, e); - return Mono.error(e); - } - - // For configuration variables, save the variables to the config collection instead of .env file - // We ideally want to migrate all variables from .env file to the config collection for better - // scalability - // Write the changes to the tenant collection in configuration field - return updateTenantConfiguration(user.getTenantId(), changes) - .then(sendAnalyticsEvent(user, originalVariables, changes)) - .thenReturn(originalVariables); - }) + .flatMap(user -> applyChangesToEnvFileWithoutAclCheck(changes) + // For configuration variables, save the variables to the config collection instead of .env file + // We ideally want to migrate all variables from .env file to the config collection for better + // scalability + // Write the changes to the tenant collection in configuration field + .flatMap(originalVariables -> updateTenantConfiguration(user.getTenantId(), changes) + .then(sendAnalyticsEvent(user, originalVariables, changes)) + .thenReturn(originalVariables))) .flatMap(originalValues -> { Mono dependentTasks = Mono.empty(); @@ -455,6 +429,45 @@ public class EnvManagerCEImpl implements EnvManagerCE { }); } + /** + * This method applies the changes to the env file and should be called internally within the server as the ACL + * checks are skipped. For client side calls please use {@link EnvManagerCEImpl#applyChanges(Map, String)}. + * Please refer {@link FeatureFlagMigrationHelper} for the use case where ACL checks + * should be skipped. + * + * @param changes Map of changes to be applied to the env file + * @return Map of original variables before the changes were applied + */ + @Override + public Mono> applyChangesToEnvFileWithoutAclCheck(Map changes) { + final Path envFilePath = Path.of(commonConfig.getEnvFilePath()); + String originalContent; + try { + originalContent = Files.readString(envFilePath); + } catch (IOException e) { + log.error("Unable to read env file " + envFilePath, e); + return Mono.error(e); + } + Map originalVariables = parseToMap(originalContent); + + final Map envFileChanges = new HashMap<>(changes); + final Set tenantConfigurationKeys = allowedTenantConfiguration(); + for (final String key : changes.keySet()) { + if (tenantConfigurationKeys.contains(key)) { + envFileChanges.remove(key); + } + } + final List changedContent = transformEnvContent(originalContent, envFileChanges); + + try { + Files.write(envFilePath, changedContent); + } catch (IOException e) { + log.error("Unable to write to env file " + envFilePath, e); + return Mono.error(e); + } + return Mono.just(originalVariables); + } + @Override public Mono applyChangesFromMultipartFormData(MultiValueMap formData, String originHeader) { return Flux.fromIterable(formData.entrySet()) @@ -656,22 +669,28 @@ public class EnvManagerCEImpl implements EnvManagerCE { @Override public Mono> getAll() { - return verifyCurrentUserIsSuper().flatMap(user -> { - final String originalContent; - try { - originalContent = Files.readString(Path.of(commonConfig.getEnvFilePath())); - } catch (NoSuchFileException e) { - return Mono.error(new AppsmithException(AppsmithError.ENV_FILE_NOT_FOUND)); - } catch (IOException e) { - log.error("Unable to read env file " + commonConfig.getEnvFilePath(), e); - return Mono.error(e); - } + return verifyCurrentUserIsSuper().then(getAllWithoutAclCheck()); + } - // set the default values to response - Map envKeyValueMap = parseToMap(originalContent); - - return Mono.justOrEmpty(envKeyValueMap); - }); + /** + * This function is used to get all the env variables from the env file and should be called internally within the + * server as the ACL checks are skipped. For client side calls please use {@link EnvManagerCEImpl#getAll()}. + * + * @return Returns a map of all the env variables + */ + @Override + public Mono> getAllWithoutAclCheck() { + String originalContent; + try { + originalContent = Files.readString(Path.of(commonConfig.getEnvFilePath())); + } catch (NoSuchFileException e) { + return Mono.error(new AppsmithException(AppsmithError.ENV_FILE_NOT_FOUND)); + } catch (IOException e) { + log.error("Unable to read env file " + commonConfig.getEnvFilePath(), e); + return Mono.error(e); + } + // set the default values to response + return Mono.just(parseToMap(originalContent)); } /** @@ -704,18 +723,27 @@ public class EnvManagerCEImpl implements EnvManagerCE { @Override public Mono restart() { - return verifyCurrentUserIsSuper().flatMap(user -> { - log.warn("Initiating restart via supervisor."); - try { - Runtime.getRuntime().exec(new String[] { - "supervisorctl", "restart", "backend", "editor", "rts", - }); - } catch (IOException e) { - log.error("Error invoking supervisorctl to restart.", e); - return Mono.error(new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR)); - } - return Mono.empty(); - }); + return verifyCurrentUserIsSuper().then(restartWithoutAclCheck()); + } + + /** + * This function is used to restart the server using supervisorctl command and should be called internally within + * the server as the ACL checks are skipped. For client side calls we should use {@link EnvManagerCEImpl#restart()} + * + * @return Returns a Mono + */ + @Override + public Mono restartWithoutAclCheck() { + log.warn("Initiating restart via supervisor."); + try { + Runtime.getRuntime().exec(new String[] { + "supervisorctl", "restart", "backend", "editor", "rts", + }); + } catch (IOException e) { + log.error("Error invoking supervisorctl to restart.", e); + return Mono.error(new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR)); + } + return Mono.empty(); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ScheduledTaskCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ScheduledTaskCEImpl.java index 27476a63b8..dd44cb5dd1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ScheduledTaskCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ScheduledTaskCEImpl.java @@ -26,7 +26,8 @@ public class ScheduledTaskCEImpl implements ScheduledTaskCE { .getAllRemoteFeaturesForTenantAndUpdateFeatureFlagsWithPendingMigrations() .then(tenantService .getDefaultTenant() - .flatMap(featureFlagService::checkAndExecuteMigrationsForTenantFeatureFlags)) + .flatMap(featureFlagService::checkAndExecuteMigrationsForTenantFeatureFlags) + .then(tenantService.restartTenant())) .doOnError(error -> log.error("Error while fetching features from Cloud Services {0}", error)) .subscribeOn(scheduler) .subscribe(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperTest.java index 0fcac27042..c39f5cd35b 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/FeatureFlagMigrationHelperTest.java @@ -25,6 +25,7 @@ import java.util.UUID; import static com.appsmith.server.constants.FeatureMigrationType.DISABLE; import static com.appsmith.server.constants.FeatureMigrationType.ENABLE; +import static com.appsmith.server.constants.MigrationStatus.PENDING; import static com.appsmith.server.featureflags.FeatureFlagEnum.TENANT_TEST_FEATURE; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -43,7 +44,7 @@ class FeatureFlagMigrationHelperTest { void setUp() {} @Test - void getUpdatedFlagsWithPendingMigration_diffForExistingAndLatestFlag_pendingMigrationReported() { + void getUpdatedFlagsWithPendingMigration_diffForExistingAndLatestFlag_pendingMigrationReportedWithDisableStatus() { Tenant defaultTenant = new Tenant(); defaultTenant.setId(UUID.randomUUID().toString()); defaultTenant.setTenantConfiguration(new TenantConfiguration()); @@ -80,6 +81,44 @@ class FeatureFlagMigrationHelperTest { .verifyComplete(); } + @Test + void getUpdatedFlagsWithPendingMigration_diffForExistingAndLatestFlag_pendingMigrationReportedWithEnableStatus() { + Tenant defaultTenant = new Tenant(); + defaultTenant.setId(UUID.randomUUID().toString()); + defaultTenant.setTenantConfiguration(new TenantConfiguration()); + + CachedFeatures existingCachedFeatures = new CachedFeatures(); + Map featureMap = new HashMap<>(); + featureMap.put(TENANT_TEST_FEATURE.name(), false); + existingCachedFeatures.setFeatures(featureMap); + existingCachedFeatures.setRefreshedAt(Instant.now().minus(1, ChronoUnit.DAYS)); + + CachedFeatures latestCachedFeatures = new CachedFeatures(); + Map latestFeatureMap = new HashMap<>(); + latestFeatureMap.put(TENANT_TEST_FEATURE.name(), true); + latestCachedFeatures.setFeatures(latestFeatureMap); + latestCachedFeatures.setRefreshedAt(Instant.now()); + + Mockito.when(cacheableFeatureFlagHelper.fetchCachedTenantFeatures(any())) + .thenReturn(Mono.just(existingCachedFeatures)) + .thenReturn(Mono.just(latestCachedFeatures)); + + Mockito.when(cacheableFeatureFlagHelper.evictCachedTenantFeatures(any())) + .thenReturn(Mono.empty()); + + Mono> getUpdatedFlagsWithPendingMigration = + featureFlagMigrationHelper.getUpdatedFlagsWithPendingMigration(defaultTenant); + + StepVerifier.create(getUpdatedFlagsWithPendingMigration) + .assertNext(featureFlagEnumFeatureMigrationTypeMap -> { + assertThat(featureFlagEnumFeatureMigrationTypeMap).isNotEmpty(); + assertThat(featureFlagEnumFeatureMigrationTypeMap.size()).isEqualTo(1); + assertThat(featureFlagEnumFeatureMigrationTypeMap.get(TENANT_TEST_FEATURE)) + .isEqualTo(ENABLE); + }) + .verifyComplete(); + } + @Test void getUpdatedFlagsWithPendingMigration_noDiffForExistingAndLatestFlag_noPendingMigrations() { Tenant defaultTenant = new Tenant(); @@ -163,6 +202,8 @@ class FeatureFlagMigrationHelperTest { Tenant defaultTenant = new Tenant(); TenantConfiguration tenantConfiguration = new TenantConfiguration(); tenantConfiguration.setFeaturesWithPendingMigration(Map.of(TENANT_TEST_FEATURE, ENABLE)); + tenantConfiguration.setMigrationStatus(PENDING); + defaultTenant.setTenantConfiguration(tenantConfiguration); CachedFeatures existingCachedFeatures = new CachedFeatures(); Map featureMap = new HashMap<>(); @@ -175,7 +216,14 @@ class FeatureFlagMigrationHelperTest { Mono resultMono = featureFlagMigrationHelper.checkAndExecuteMigrationsForFeatureFlag(defaultTenant, TENANT_TEST_FEATURE); StepVerifier.create(resultMono) - .assertNext(result -> assertThat(result).isTrue()) + .assertNext(result -> { + assertThat(result).isTrue(); + assertThat(tenantConfiguration + .getFeaturesWithPendingMigration() + .size()) + .isEqualTo(1); + assertThat(tenantConfiguration.getMigrationStatus()).isEqualTo(PENDING); + }) .verifyComplete(); } }