chore: Code split service classes for enabling feature based migrations for SSO (#27404)

This commit is contained in:
Abhijeet 2023-09-29 12:32:50 +05:30 committed by GitHub
parent 751e0330c1
commit e1e45a32b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 505 additions and 326 deletions

View File

@ -47,6 +47,10 @@ public class TenantConfigurationCE {
// 2. Because of grandfathering via cron where tenant level feature flags are fetched
Map<FeatureFlagEnum, FeatureMigrationType> 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<>();

View File

@ -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<Map<FeatureFlagEnum, FeatureMigrationType>> 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<Map<FeatureFlagEnum, FeatureMigrationType>> 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<CachedFeatures> 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<FeatureFlagEnum, FeatureMigrationType> 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<FeatureFlagEnum, FeatureMigrationType> featureDiffsWithMigrationType = new HashMap<>();
Map<String, Boolean> 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<FeatureFlagEnum, FeatureMigrationType> getUpdatedFlagsWithPendingMigration(
Map<FeatureFlagEnum, FeatureMigrationType> latestFeatureDiffsWithMigrationType, Tenant dbTenant) {
Map<FeatureFlagEnum, FeatureMigrationType> featuresWithPendingMigrationDB =
dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration() == null
? new HashMap<>()
: dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration();
Map<FeatureFlagEnum, FeatureMigrationType> 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<Boolean> 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<Boolean> isMigrationRequired(Tenant tenant, FeatureFlagEnum featureFlagEnum) {
Map<FeatureFlagEnum, FeatureMigrationType> featureMigrationTypeMap =
tenant.getTenantConfiguration().getFeaturesWithPendingMigration();
if (CollectionUtils.isNullOrEmpty(featureMigrationTypeMap)) {
return Mono.just(FALSE);
}
return cacheableFeatureFlagHelper
.fetchCachedTenantFeatures(tenant.getId())
.map(cachedFeatures -> {
Map<String, Boolean> 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<Boolean> executeMigrationsBasedOnFeatureFlag(
TenantConfiguration tenantConfiguration, FeatureFlagEnum featureFlagEnum) {
// TODO implement migrations as per the supported features in license plan
Map<FeatureFlagEnum, FeatureMigrationType> 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 {}

View File

@ -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);
}
}

View File

@ -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<Map<FeatureFlagEnum, FeatureMigrationType>> getUpdatedFlagsWithPendingMigration(Tenant defaultTenant);
Mono<Map<FeatureFlagEnum, FeatureMigrationType>> getUpdatedFlagsWithPendingMigration(
Tenant tenant, boolean forceUpdate);
Mono<Boolean> checkAndExecuteMigrationsForFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum);
Mono<Boolean> executeMigrationsBasedOnFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum);
}

View File

@ -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<Map<FeatureFlagEnum, FeatureMigrationType>> 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<Map<FeatureFlagEnum, FeatureMigrationType>> 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<CachedFeatures> 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<FeatureFlagEnum, FeatureMigrationType> 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<FeatureFlagEnum, FeatureMigrationType> featureDiffsWithMigrationType = new HashMap<>();
Map<String, Boolean> 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<FeatureFlagEnum, FeatureMigrationType> getUpdatedFlagsWithPendingMigration(
Map<FeatureFlagEnum, FeatureMigrationType> latestFeatureDiffsWithMigrationType, Tenant dbTenant) {
Map<FeatureFlagEnum, FeatureMigrationType> featuresWithPendingMigrationDB =
dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration() == null
? new HashMap<>()
: dbTenant.getTenantConfiguration().getFeaturesWithPendingMigration();
Map<FeatureFlagEnum, FeatureMigrationType> 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<Boolean> 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<FeatureFlagEnum, FeatureMigrationType> 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<Boolean> isMigrationRequired(Tenant tenant, FeatureFlagEnum featureFlagEnum) {
Map<FeatureFlagEnum, FeatureMigrationType> featureMigrationTypeMap =
tenant.getTenantConfiguration().getFeaturesWithPendingMigration();
if (CollectionUtils.isNullOrEmpty(featureMigrationTypeMap)) {
return Mono.just(FALSE);
}
return cacheableFeatureFlagHelper
.fetchCachedTenantFeatures(tenant.getId())
.map(cachedFeatures -> {
Map<String, Boolean> 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<Boolean> executeMigrationsBasedOnFeatureFlag(Tenant tenant, FeatureFlagEnum featureFlagEnum) {
return Mono.just(TRUE);
}
}

View File

@ -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<List<String>> getSessionKeysByUserEmail(String email);
Mono<Long> deleteSessionsByKeys(List<String> keys);
Flux<Tuple2<String, User>> getSessionKeysWithUserSessions();
}

View File

@ -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<List<String>> 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<Tuple2<String, User>> 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

View File

@ -23,4 +23,8 @@ public interface TenantServiceCE extends CrudService<Tenant, String> {
Mono<Tenant> save(Tenant tenant);
Mono<Tenant> checkAndExecuteMigrationsForTenantFeatureFlags(Tenant tenant);
Mono<Tenant> retrieveById(String id);
Mono<Void> restartTenant();
}

View File

@ -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<TenantRepository, Tenant, String> implements TenantServiceCE {
private String tenantId = null;
@ -233,6 +235,35 @@ public class TenantServiceCEImpl extends BaseService<TenantRepository, Tenant, S
});
}
@Override
public Mono<Tenant> 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<Void> restartTenant() {
// Avoid dependency on user context as this method will be called internally by the server
Mono<Tenant> 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(

View File

@ -16,6 +16,8 @@ public interface EnvManagerCE {
Mono<Void> applyChanges(Map<String, String> changes, String originHeader);
Mono<Map<String, String>> applyChangesToEnvFileWithoutAclCheck(Map<String, String> changes);
Mono<Void> applyChangesFromMultipartFormData(MultiValueMap<String, Part> formData, String originHeader);
void setAnalyticsEventAction(
@ -25,6 +27,8 @@ public interface EnvManagerCE {
Map<String, String> parseToMap(String content);
Mono<Map<String, String>> getAllWithoutAclCheck();
Mono<Map<String, String>> getAll();
Mono<Map<String, String>> getAllNonEmpty();
@ -33,6 +37,8 @@ public interface EnvManagerCE {
Mono<Void> restart();
Mono<Void> restartWithoutAclCheck();
Mono<Boolean> sendTestEmail(TestEmailConfigRequestDTO requestDTO);
Mono<Void> download(ServerWebExchange exchange);

View File

@ -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<Void> validateChanges(User user, Map<String, String> 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<String> 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<String, String> originalVariables = parseToMap(originalContent);
final Map<String, String> envFileChanges = new HashMap<>(changes);
final Set<String> tenantConfigurationKeys = allowedTenantConfiguration();
for (final String key : changes.keySet()) {
if (tenantConfigurationKeys.contains(key)) {
envFileChanges.remove(key);
}
}
final List<String> 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<Void> 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<Map<String, String>> applyChangesToEnvFileWithoutAclCheck(Map<String, String> 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<String, String> originalVariables = parseToMap(originalContent);
final Map<String, String> envFileChanges = new HashMap<>(changes);
final Set<String> tenantConfigurationKeys = allowedTenantConfiguration();
for (final String key : changes.keySet()) {
if (tenantConfigurationKeys.contains(key)) {
envFileChanges.remove(key);
}
}
final List<String> 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<Void> applyChangesFromMultipartFormData(MultiValueMap<String, Part> formData, String originHeader) {
return Flux.fromIterable(formData.entrySet())
@ -656,22 +669,28 @@ public class EnvManagerCEImpl implements EnvManagerCE {
@Override
public Mono<Map<String, String>> 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<String, String> 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<Map<String, String>> 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<Void> 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<Void>
*/
@Override
public Mono<Void> 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

View File

@ -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();

View File

@ -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<String, Boolean> 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<String, Boolean> 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<Map<FeatureFlagEnum, FeatureMigrationType>> 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<String, Boolean> featureMap = new HashMap<>();
@ -175,7 +216,14 @@ class FeatureFlagMigrationHelperTest {
Mono<Boolean> 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();
}
}