1   /*
2    * Copyright 2018 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.internal.api;
18  
19  import java.util.concurrent.CompletableFuture;
20  
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  
24  import com.fasterxml.jackson.annotation.JsonProperty;
25  import com.fasterxml.jackson.databind.JsonNode;
26  
27  import com.linecorp.armeria.common.HttpStatus;
28  import com.linecorp.armeria.server.HttpStatusException;
29  import com.linecorp.armeria.server.ServiceRequestContext;
30  import com.linecorp.armeria.server.annotation.Consumes;
31  import com.linecorp.armeria.server.annotation.Get;
32  import com.linecorp.armeria.server.annotation.Patch;
33  import com.linecorp.armeria.server.annotation.ProducesJson;
34  import com.linecorp.centraldogma.internal.Jackson;
35  import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
36  import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchException;
37  import com.linecorp.centraldogma.server.command.CommandExecutor;
38  import com.linecorp.centraldogma.server.internal.api.auth.RequiresAdministrator;
39  
40  @ProducesJson
41  public final class AdministrativeService extends AbstractService {
42  
43      private static final Logger logger = LoggerFactory.getLogger(AdministrativeService.class);
44  
45      public AdministrativeService(CommandExecutor executor) {
46          super(executor);
47      }
48  
49      /**
50       * GET /status
51       *
52       * <p>Returns the server status.
53       */
54      @Get("/status")
55      public ServerStatus status() {
56          return new ServerStatus(executor().isWritable(), executor().isStarted());
57      }
58  
59      /**
60       * PATCH /status
61       *
62       * <p>Patches the server status with a JSON patch. Currently used only for entering read-only.
63       */
64      @Patch("/status")
65      @Consumes("application/json-patch+json")
66      @RequiresAdministrator
67      public CompletableFuture<ServerStatus> updateStatus(ServiceRequestContext ctx,
68                                                          JsonNode patch) throws Exception {
69          // TODO(trustin): Consider extracting this into common utility or Armeria.
70          final ServerStatus oldStatus = status();
71          final JsonNode oldValue = Jackson.valueToTree(oldStatus);
72          final JsonNode newValue;
73          try {
74              newValue = JsonPatch.fromJson(patch).apply(oldValue);
75          } catch (JsonPatchException e) {
76              // Failed to apply the given JSON patch.
77              return rejectStatusPatch(patch);
78          }
79  
80          if (!newValue.isObject()) {
81              return rejectStatusPatch(patch);
82          }
83  
84          final JsonNode writableNode = newValue.get("writable");
85          final JsonNode replicatingNode = newValue.get("replicating");
86          if (!writableNode.isBoolean() || !replicatingNode.isBoolean()) {
87              return rejectStatusPatch(patch);
88          }
89  
90          //    writable       | replicating   |  result
91          //-------------------+---------------+-----------------------------------
92          //    true -> true   | true -> true  | not_modified exception
93          //                   | true -> false | bad_request exception
94          //-------------------+---------------+-----------------------------------
95          //    true -> false  | true -> true  | setWritable(false)
96          //                   | true -> false | setWritable(false) & stop executor.
97          //-------------------+---------------+-----------------------------------
98          //    false -> true  | true -> true  | setWritable(true)
99          //                   | false -> true | setWritable(true) & start executor.
100         //-------------------+---------------+-----------------------------------
101         //    false -> false | true -> false | Stop executor.
102         //                   | false -> true | Start executor.
103         final boolean writable = writableNode.asBoolean();
104         final boolean replicating = replicatingNode.asBoolean();
105         if (writable && !replicating) {
106             return HttpApiUtil.throwResponse(ctx, HttpStatus.BAD_REQUEST,
107                                              "'replicating' must be 'true' if 'writable' is 'true'.");
108         }
109         if (oldStatus.writable == writable && oldStatus.replicating == replicating) {
110             throw HttpStatusException.of(HttpStatus.NOT_MODIFIED);
111         }
112 
113         if (oldStatus.writable != writable) {
114             executor().setWritable(writable);
115             if (writable) {
116                 logger.warn("Left read-only mode.");
117             } else {
118                 logger.warn("Entered read-only mode. replication: {}", replicating);
119             }
120         }
121 
122         if (oldStatus.replicating != replicating) {
123             if (replicating) {
124                 return executor().start().handle((unused, cause) -> {
125                     if (cause != null) {
126                         logger.warn("Failed to start the command executor:", cause);
127                     } else {
128                         logger.info("Enabled replication. read-only: {}", !writable);
129                     }
130                     return status();
131                 });
132             }
133             return executor().stop().handle((unused, cause) -> {
134                 if (cause != null) {
135                     logger.warn("Failed to stop the command executor:", cause);
136                 } else {
137                     logger.info("Disabled replication");
138                 }
139                 return status();
140             });
141         }
142 
143         return CompletableFuture.completedFuture(status());
144     }
145 
146     private static CompletableFuture<ServerStatus> rejectStatusPatch(JsonNode patch) {
147         throw new IllegalArgumentException("Invalid JSON patch: " + patch);
148     }
149 
150     // TODO(trustin): Add more properties, e.g. method, host name, isLeader and config.
151     private static final class ServerStatus {
152         @JsonProperty
153         final boolean writable;
154         @JsonProperty
155         final boolean replicating;
156 
157         ServerStatus(boolean writable, boolean replicating) {
158             assert !writable || replicating; // replicating must be true if writable is true.
159             this.writable = writable;
160             this.replicating = replicating;
161         }
162     }
163 }