Server restart API, and restart detection API (#7480)
This commit is contained in:
parent
1fa4a175a3
commit
09c2875e4f
|
|
@ -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*"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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:}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user