Server restart API, and restart detection API (#7480)

This commit is contained in:
Shrikant Sharat Kandula 2021-10-11 16:29:39 +05:30 committed by GitHub
parent 1fa4a175a3
commit 09c2875e4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 35 deletions

View File

@ -3,8 +3,10 @@ package com.appsmith.server.configurations;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -27,10 +29,13 @@ public class CommonConfig {
private static final String ELASTIC_THREAD_POOL_NAME = "appsmith-elastic-pool";
@Value("${appsmith.instance.name:}")
private String instanceName;
@Value("${signup.disabled}")
private boolean isSignupDisabled;
@Value("${admin.emails}")
@Setter(AccessLevel.NONE)
private Set<String> adminEmails = Collections.emptySet();
@Value("${oauth2.allowed-domains}")
@ -52,6 +57,8 @@ public class CommonConfig {
@Value("${appsmith.admin.envfile:}")
public String envFilePath;
@Value("${disable.telemetry:true}")
private boolean isTelemetryDisabled;
private List<String> allowedDomains;
@ -93,4 +100,10 @@ public class CommonConfig {
return allowedDomains;
}
@Autowired
public void setAdminEmails(@Value("${admin.emails}") String value) {
adminEmails = Set.of(value.trim().split("\\s*,\\s*"));
}
}

View File

@ -1,13 +1,15 @@
package com.appsmith.server.configurations;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
public class GoogleRecaptchaConfig {
@Value("${google.recaptcha.key.site}")
private String siteKey;

View File

@ -1,12 +1,14 @@
package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.dtos.EnvChangesResponseDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.solutions.EnvManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -32,11 +34,25 @@ public class InstanceAdminController {
}
@PutMapping("/env")
public Mono<ResponseDTO<Boolean>> saveEnvChanges(
public Mono<ResponseDTO<EnvChangesResponseDTO>> saveEnvChanges(
@Valid @RequestBody Map<String, String> changes
) {
log.debug("Applying env updates {}", changes);
return envManager.applyChanges(changes)
.map(res -> new ResponseDTO<>(HttpStatus.OK.value(), res, null));
}
@PostMapping("/restart")
public Mono<ResponseDTO<Boolean>> restart() {
log.debug("Received restart request");
return envManager.restart()
.thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null));
}
@PostMapping("/send-test-email")
public Mono<ResponseDTO<Boolean>> sendTestEmail() {
log.debug("Sending test email");
return envManager.sendTestEmail()
.thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null));
}

View File

@ -0,0 +1,14 @@
package com.appsmith.server.dtos;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class EnvChangesResponseDTO {
@JsonProperty(value = "isRestartRequired")
boolean isRestartRequired;
}

View File

@ -40,6 +40,10 @@ public class EmailSender {
REPLY_TO = makeReplyTo();
}
public Mono<Boolean> sendMail(String to, String subject, String text) {
return sendMail(to, subject, text, null);
}
public Mono<Boolean> sendMail(String to, String subject, String text, Map<String, ? extends Object> params) {
/**
@ -51,7 +55,7 @@ public class EmailSender {
*/
Mono.fromCallable(() -> {
try {
return TemplateUtils.parseTemplate(text, params);
return params == null ? text : TemplateUtils.parseTemplate(text, params);
} catch (IOException e) {
throw Exceptions.propagate(e);
}

View File

@ -2,21 +2,29 @@ package com.appsmith.server.solutions;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.configurations.EmailConfig;
import com.appsmith.server.configurations.GoogleRecaptchaConfig;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.EnvChangesResponseDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -25,6 +33,7 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Component
@RequiredArgsConstructor
@ -34,7 +43,12 @@ public class EnvManager {
private final SessionUserService sessionUserService;
private final UserService userService;
private final PolicyUtils policyUtils;
private final EmailSender emailSender;
private final CommonConfig commonConfig;
private final EmailConfig emailConfig;
private final JavaMailSender javaMailSender;
private final GoogleRecaptchaConfig googleRecaptchaConfig;
/**
* This regex pattern matches environment variable declarations like `VAR_NAME=value` or `VAR_NAME="value"` or just
@ -45,30 +59,35 @@ public class EnvManager {
"^(?<name>[A-Z0-9_]+)\\s*=\\s*\"?(?<value>.*?)\"?$"
);
private static final Set<String> VARIABLE_WHITELIST = Set.of(
"APPSMITH_INSTANCE_NAME",
"APPSMITH_MONGODB_URI",
"APPSMITH_REDIS_URL",
"APPSMITH_MAIL_ENABLED",
"APPSMITH_MAIL_FROM",
"APPSMITH_REPLY_TO",
"APPSMITH_MAIL_HOST",
"APPSMITH_MAIL_PORT",
"APPSMITH_MAIL_USERNAME",
"APPSMITH_MAIL_PASSWORD",
"APPSMITH_MAIL_SMTP_TLS_ENABLED",
"APPSMITH_SIGNUP_DISABLED",
"APPSMITH_SIGNUP_ALLOWED_DOMAINS",
"APPSMITH_ADMIN_EMAILS",
"APPSMITH_RECAPTCHA_SITE_KEY",
"APPSMITH_RECAPTCHA_SECRET_KEY",
"APPSMITH_GOOGLE_MAPS_API_KEY",
"APPSMITH_DISABLE_TELEMETRY",
"APPSMITH_OAUTH2_GOOGLE_CLIENT_ID",
"APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET",
"APPSMITH_OAUTH2_GITHUB_CLIENT_ID",
"APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET"
);
private enum Vars {
APPSMITH_INSTANCE_NAME,
APPSMITH_MONGODB_URI,
APPSMITH_REDIS_URL,
APPSMITH_MAIL_ENABLED,
APPSMITH_MAIL_FROM,
APPSMITH_REPLY_TO,
APPSMITH_MAIL_HOST,
APPSMITH_MAIL_PORT,
APPSMITH_MAIL_SMTP_AUTH,
APPSMITH_MAIL_USERNAME,
APPSMITH_MAIL_PASSWORD,
APPSMITH_MAIL_SMTP_TLS_ENABLED,
APPSMITH_SIGNUP_DISABLED,
APPSMITH_SIGNUP_ALLOWED_DOMAINS,
APPSMITH_ADMIN_EMAILS,
APPSMITH_RECAPTCHA_SITE_KEY,
APPSMITH_RECAPTCHA_SECRET_KEY,
APPSMITH_GOOGLE_MAPS_API_KEY,
APPSMITH_DISABLE_TELEMETRY,
APPSMITH_OAUTH2_GOOGLE_CLIENT_ID,
APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET,
APPSMITH_OAUTH2_GITHUB_CLIENT_ID,
APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET,
}
private static final Set<String> VARIABLE_WHITELIST = Stream.of(Vars.values())
.map(Enum::name)
.collect(Collectors.toUnmodifiableSet());
/**
* Updates values of variables in the envContent string, based on the changes map given. This function **only**
@ -86,12 +105,18 @@ public class EnvManager {
throw new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS);
}
if (changes.containsKey("APPSMITH_MAIL_HOST")) {
changes.put("APPSMITH_MAIL_ENABLED", Boolean.toString(StringUtils.isEmpty(changes.get("APPSMITH_MAIL_HOST"))));
if (changes.containsKey(Vars.APPSMITH_MAIL_HOST.name())) {
changes.put(
Vars.APPSMITH_MAIL_ENABLED.name(),
Boolean.toString(StringUtils.isNotEmpty(changes.get(Vars.APPSMITH_MAIL_HOST.name())))
);
}
if (changes.containsKey("APPSMITH_MAIL_USERNAME")) {
changes.put("APPSMITH_MAIL_SMTP_AUTH", Boolean.toString(StringUtils.isEmpty(changes.get("APPSMITH_MAIL_USERNAME"))));
if (changes.containsKey(Vars.APPSMITH_MAIL_USERNAME.name())) {
changes.put(
Vars.APPSMITH_MAIL_SMTP_AUTH.name(),
Boolean.toString(StringUtils.isNotEmpty(changes.get(Vars.APPSMITH_MAIL_USERNAME.name())))
);
}
final Set<String> remainingChangedNames = new HashSet<>(changes.keySet());
@ -120,9 +145,10 @@ public class EnvManager {
return outLines;
}
public Mono<Void> applyChanges(Map<String, String> changes) {
public Mono<EnvChangesResponseDTO> applyChanges(Map<String, String> changes) {
return verifyCurrentUserIsSuper()
.flatMap(user -> {
// Write the changes to the env file.
final String originalContent;
final Path envFilePath = Path.of(commonConfig.getEnvFilePath());
@ -142,7 +168,82 @@ public class EnvManager {
return Mono.error(e);
}
return Mono.empty();
return Mono.just(user);
})
.flatMap(user -> {
// Try and update any at runtime, that can be.
final Map<String, String> changesCopy = new HashMap<>(changes);
if (changesCopy.containsKey(Vars.APPSMITH_INSTANCE_NAME.name())) {
commonConfig.setInstanceName(changesCopy.remove(Vars.APPSMITH_INSTANCE_NAME.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_SIGNUP_DISABLED.name())) {
commonConfig.setSignupDisabled("true".equals(changesCopy.remove(Vars.APPSMITH_SIGNUP_DISABLED.name())));
}
if (changesCopy.containsKey(Vars.APPSMITH_SIGNUP_ALLOWED_DOMAINS.name())) {
commonConfig.setAllowedDomainsString(changesCopy.remove(Vars.APPSMITH_SIGNUP_ALLOWED_DOMAINS.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_ADMIN_EMAILS.name())) {
commonConfig.setAdminEmails(changesCopy.remove(Vars.APPSMITH_ADMIN_EMAILS.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_FROM.name())) {
emailConfig.setMailFrom(changesCopy.remove(Vars.APPSMITH_MAIL_FROM.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_REPLY_TO.name())) {
emailConfig.setReplyTo(changesCopy.remove(Vars.APPSMITH_REPLY_TO.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_ENABLED.name())) {
emailConfig.setEmailEnabled("true".equals(changesCopy.remove(Vars.APPSMITH_MAIL_ENABLED.name())));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_SMTP_AUTH.name())) {
emailConfig.setEmailEnabled("true".equals(changesCopy.remove(Vars.APPSMITH_MAIL_SMTP_AUTH.name())));
}
if (javaMailSender instanceof JavaMailSenderImpl) {
JavaMailSenderImpl javaMailSenderImpl = (JavaMailSenderImpl) javaMailSender;
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_HOST.name())) {
javaMailSenderImpl.setHost(changesCopy.remove(Vars.APPSMITH_MAIL_HOST.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_PORT.name())) {
javaMailSenderImpl.setPort(Integer.parseInt(changesCopy.remove(Vars.APPSMITH_MAIL_PORT.name())));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_USERNAME.name())) {
javaMailSenderImpl.setUsername(changesCopy.remove(Vars.APPSMITH_MAIL_USERNAME.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_MAIL_PASSWORD.name())) {
javaMailSenderImpl.setPassword(changesCopy.remove(Vars.APPSMITH_MAIL_PASSWORD.name()));
}
}
if (changesCopy.containsKey(Vars.APPSMITH_RECAPTCHA_SITE_KEY.name())) {
googleRecaptchaConfig.setSiteKey(changesCopy.remove(Vars.APPSMITH_RECAPTCHA_SITE_KEY.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_RECAPTCHA_SECRET_KEY.name())) {
googleRecaptchaConfig.setSecretKey(changesCopy.remove(Vars.APPSMITH_RECAPTCHA_SECRET_KEY.name()));
}
if (changesCopy.containsKey(Vars.APPSMITH_DISABLE_TELEMETRY.name())) {
commonConfig.setTelemetryDisabled("true".equals(changesCopy.remove(Vars.APPSMITH_DISABLE_TELEMETRY.name())));
}
// Ideally, we should only need a restart here if `changesCopy` is not empty. However, some of these
// env variables are also used in client code, which means restart might be necessary there. So, to
// provide a more uniform and predictable experience, we always restart.
Mono.delay(Duration.ofSeconds(1))
.flatMap(ignored -> restart())
.subscribeOn(Schedulers.boundedElastic())
.subscribe();
return Mono.just(new EnvChangesResponseDTO(true));
});
}
@ -189,4 +290,33 @@ public class EnvManager {
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS)));
}
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();
});
}
public Mono<Boolean> sendTestEmail() {
return sessionUserService.getCurrentUser()
.flatMap(user -> emailSender.sendMail(
user.getEmail(),
"Test email from Appsmith",
"This is a test email from Appsmith, initiated from Admin Settings page. If you are seeing this, your email configuration is working!\n"
));
}
}

View File

@ -1,5 +1,6 @@
package com.appsmith.server.solutions;
import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.configurations.SegmentConfig;
import com.appsmith.server.helpers.NetworkUtils;
import com.appsmith.server.services.ConfigService;
@ -23,7 +24,7 @@ import java.util.Map;
* permissions to collect anonymized data
*/
@Component
@ConditionalOnExpression("!${is.cloud-hosted:false} && !${disable.telemetry:true}")
@ConditionalOnExpression("!${is.cloud-hosted:false}")
@Slf4j
@RequiredArgsConstructor
public class PingScheduledTask {
@ -32,6 +33,8 @@ public class PingScheduledTask {
private final SegmentConfig segmentConfig;
private final CommonConfig commonConfig;
/**
* Gets the external IP address of this server and pings a data point to indicate that this server instance is live.
* We use an initial delay of two minutes to roughly wait for the application along with the migrations are finished
@ -40,6 +43,10 @@ public class PingScheduledTask {
// Number of milliseconds between the start of each scheduled calls to this method.
@Scheduled(initialDelay = 2 * 60 * 1000 /* two minutes */, fixedRate = 6 * 60 * 60 * 1000 /* six hours */)
public void pingSchedule() {
if (commonConfig.isTelemetryDisabled()) {
return;
}
Mono.zip(configService.getInstanceId(), NetworkUtils.getExternalAddress())
.flatMap(tuple -> doPing(tuple.getT1(), tuple.getT2()))
.subscribeOn(Schedulers.single())

View File

@ -105,3 +105,6 @@ appsmith.plugin.response.size.max=${APPSMITH_PLUGIN_MAX_RESPONSE_SIZE_MB:5}
# Location env file with environment variables, that can be configured from the UI.
appsmith.admin.envfile=${APPSMITH_ENVFILE_PATH:/appsmith-stacks/configuration/docker.env}
# Name of this instance
appsmith.instance.name=${APPSMITH_INSTANCE_NAME:}