blob: 4fdf73283b047408062d8e96c2a9284f0a55ac4e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.rest;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.net.BindException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.apache.ignite.configuration.ConfigurationRegistry;
import org.apache.ignite.configuration.internal.util.ConfigurationUtil;
import org.apache.ignite.configuration.validation.ConfigurationValidationException;
import org.apache.ignite.rest.configuration.RestConfiguration;
import org.apache.ignite.rest.configuration.RestView;
import org.apache.ignite.rest.netty.RestApiInitializer;
import org.apache.ignite.rest.presentation.ConfigurationPresentation;
import org.apache.ignite.rest.presentation.json.JsonConverter;
import org.apache.ignite.rest.presentation.json.JsonPresentation;
import org.apache.ignite.rest.routes.Router;
import org.slf4j.Logger;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
/**
* Rest module is responsible for starting a REST endpoints for accessing and managing configuration.
*
* It is started on port 10300 by default but it is possible to change this in configuration itself.
* Refer to default config file in resources for the example.
*/
public class RestModule {
/** */
public static final int DFLT_PORT = 10300;
/** */
private static final String CONF_URL = "/management/v1/configuration/";
/** */
private static final String PATH_PARAM = "selector";
/** */
private ConfigurationRegistry sysConf;
/** */
private volatile ConfigurationPresentation<String> presentation;
/** */
private final Logger log;
/** */
public RestModule(Logger log) {
this.log = log;
}
/** */
public void prepareStart(ConfigurationRegistry sysCfg) {
sysConf = sysCfg;
sysCfg.registerRootKey(RestConfiguration.KEY);
presentation = new JsonPresentation();
}
/**
*
*/
public ChannelFuture start() throws InterruptedException {
var router = new Router();
router
.get(CONF_URL, (req, resp) -> {
resp.json(presentation.represent());
})
.get(CONF_URL + ":" + PATH_PARAM, (req, resp) -> {
String cfgPath = req.queryParams().get(PATH_PARAM);
try {
List<String> path = ConfigurationUtil.split(cfgPath);
JsonElement json = sysConf.represent(path, JsonConverter.jsonVisitor());
resp.json(json);
}
catch (IllegalArgumentException pathE) {
ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", pathE.getMessage());
resp.status(BAD_REQUEST);
resp.json(Map.of("error", eRes));
}
})
.put(CONF_URL, HttpHeaderValues.APPLICATION_JSON, (req, resp) -> {
try {
presentation.update(
req
.request()
.content()
.readCharSequence(req.request().content().readableBytes(), StandardCharsets.UTF_8)
.toString());
}
catch (IllegalArgumentException argE) {
ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", argE.getMessage());
resp.status(BAD_REQUEST);
resp.json(Map.of("error", eRes));
}
catch (ConfigurationValidationException validationE) {
ErrorResult eRes = new ErrorResult("APPLICATION_EXCEPTION", validationE.getMessage());
resp.status(BAD_REQUEST);
resp.json(Map.of("error", eRes));
resp.json(eRes);
}
catch (JsonSyntaxException e) {
String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", msg);
resp.status(BAD_REQUEST);
resp.json(Map.of("error", eRes));
}
catch (Exception e) {
ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", e.getMessage());
resp.status(BAD_REQUEST);
resp.json(Map.of("error", eRes));
}
});
return startRestEndpoint(router);
}
/** */
private ChannelFuture startRestEndpoint(Router router) throws InterruptedException {
RestView restConfigurationView = sysConf.getConfiguration(RestConfiguration.KEY).value();
int desiredPort = restConfigurationView.port();
int portRange = restConfigurationView.portRange();
int port = 0;
Channel ch = null;
EventLoopGroup parentGrp = new NioEventLoopGroup();
EventLoopGroup childGrp = new NioEventLoopGroup();
var hnd = new RestApiInitializer(router);
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(parentGrp, childGrp)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(hnd);
for (int portCandidate = desiredPort; portCandidate < desiredPort + portRange; portCandidate++) {
ChannelFuture bindRes = b.bind(portCandidate).await();
if (bindRes.isSuccess()) {
ch = bindRes.channel();
ch.closeFuture().addListener(new ChannelFutureListener() {
@Override public void operationComplete(ChannelFuture fut) {
parentGrp.shutdownGracefully();
childGrp.shutdownGracefully();
}
});
port = portCandidate;
break;
}
else if (!(bindRes.cause() instanceof BindException)) {
parentGrp.shutdownGracefully();
childGrp.shutdownGracefully();
throw new RuntimeException(bindRes.cause());
}
}
if (ch == null) {
String msg = "Cannot start REST endpoint. " +
"All ports in range [" + desiredPort + ", " + (desiredPort + portRange) + "] are in use.";
log.error(msg);
parentGrp.shutdownGracefully();
childGrp.shutdownGracefully();
throw new RuntimeException(msg);
}
log.info("REST protocol started successfully on port " + port);
return ch.closeFuture();
}
}