From 09c2875e4fb2b283a09b79b67f4e0aa53377f191 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Mon, 11 Oct 2021 16:29:39 +0530 Subject: [PATCH] Server restart API, and restart detection API (#7480) --- .../server/configurations/CommonConfig.java | 15 +- .../configurations/GoogleRecaptchaConfig.java | 4 +- .../controllers/InstanceAdminController.java | 18 +- .../server/dtos/EnvChangesResponseDTO.java | 14 ++ .../server/notifications/EmailSender.java | 6 +- .../appsmith/server/solutions/EnvManager.java | 190 +++++++++++++++--- .../server/solutions/PingScheduledTask.java | 9 +- .../src/main/resources/application.properties | 3 + 8 files changed, 224 insertions(+), 35 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/EnvChangesResponseDTO.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java index 5e207ba383..4489e7a134 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/CommonConfig.java @@ -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 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 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*")); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/GoogleRecaptchaConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/GoogleRecaptchaConfig.java index cecf682768..f2554e9801 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/GoogleRecaptchaConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/GoogleRecaptchaConfig.java @@ -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; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/InstanceAdminController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/InstanceAdminController.java index cfc0ba1931..744d7ecb7e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/InstanceAdminController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/InstanceAdminController.java @@ -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> saveEnvChanges( + public Mono> saveEnvChanges( @Valid @RequestBody Map changes ) { log.debug("Applying env updates {}", changes); return envManager.applyChanges(changes) + .map(res -> new ResponseDTO<>(HttpStatus.OK.value(), res, null)); + } + + @PostMapping("/restart") + public Mono> restart() { + log.debug("Received restart request"); + return envManager.restart() + .thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null)); + } + + @PostMapping("/send-test-email") + public Mono> sendTestEmail() { + log.debug("Sending test email"); + return envManager.sendTestEmail() .thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/EnvChangesResponseDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/EnvChangesResponseDTO.java new file mode 100644 index 0000000000..48f44ec26d --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/EnvChangesResponseDTO.java @@ -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; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java index 29852477d6..51b9623f47 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java @@ -40,6 +40,10 @@ public class EmailSender { REPLY_TO = makeReplyTo(); } + public Mono sendMail(String to, String subject, String text) { + return sendMail(to, subject, text, null); + } + public Mono sendMail(String to, String subject, String text, Map 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); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/EnvManager.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/EnvManager.java index 8f91feb00d..8f6ffffad9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/EnvManager.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/EnvManager.java @@ -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 { "^(?[A-Z0-9_]+)\\s*=\\s*\"?(?.*?)\"?$" ); - private static final Set 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 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 remainingChangedNames = new HashSet<>(changes.keySet()); @@ -120,9 +145,10 @@ public class EnvManager { return outLines; } - public Mono applyChanges(Map changes) { + public Mono applyChanges(Map 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 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 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 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" + )); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java index dc49569a15..3a24b5e2b1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java @@ -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()) diff --git a/app/server/appsmith-server/src/main/resources/application.properties b/app/server/appsmith-server/src/main/resources/application.properties index aab6043c39..512619ff1c 100644 --- a/app/server/appsmith-server/src/main/resources/application.properties +++ b/app/server/appsmith-server/src/main/resources/application.properties @@ -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:}