1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
51
52
53
54 @Get("/status")
55 public ServerStatus status() {
56 return new ServerStatus(executor().isWritable(), executor().isStarted());
57 }
58
59
60
61
62
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
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
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
91
92
93
94
95
96
97
98
99
100
101
102
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
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;
159 this.writable = writable;
160 this.replicating = replicating;
161 }
162 }
163 }