/**
* Copyright (C) 2014-2015 LinkedIn Corp. (pinot-core@linkedin.com)
*
* Licensed 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 com.linkedin.pinot.controller.api.restlet.resources;
import java.util.HashSet;
import java.util.Set;
import com.linkedin.pinot.common.metrics.ControllerMeter;
import com.linkedin.pinot.controller.api.ControllerRestApplication;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.representation.Variant;
import org.restlet.resource.Delete;
import org.restlet.resource.Get;
import org.restlet.resource.Post;
import org.restlet.resource.Put;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.ByteStreams;
import com.linkedin.pinot.common.config.Tenant;
import com.linkedin.pinot.common.utils.TenantRole;
import com.linkedin.pinot.common.restlet.swagger.Description;
import com.linkedin.pinot.common.restlet.swagger.HttpVerb;
import com.linkedin.pinot.common.restlet.swagger.Parameter;
import com.linkedin.pinot.common.restlet.swagger.Paths;
import com.linkedin.pinot.common.restlet.swagger.Summary;
import com.linkedin.pinot.common.restlet.swagger.Tags;
import com.linkedin.pinot.controller.helix.core.PinotResourceManagerResponse;
import com.linkedin.pinot.controller.helix.core.PinotResourceManagerResponse.ResponseStatus;
/**
* Sample curl call to create broker tenant
* curl -i -X POST -H 'Content-Type: application/json' -d
* '{
* "role" : "broker",
* "numberOfInstances : "5",
* "name" : "brokerOne"
* }' http://lva1-pinot-controller-vip-1.corp.linkedin.com:11984/tenants
*
* Sample curl call to create server tenant
* curl -i -X POST -H 'Content-Type: application/json' -d
* '{
* "role" : "server",
* "numberOfInstances : "5",
* "name" : "serverOne",
* "offlineInstances" : "3",
* "realtimeInstances" : "2"
* }' http://lva1-pinot-controller-vip-1.corp.linkedin.com:11984/tenants
*/
public class PinotTenantRestletResource extends BasePinotControllerRestletResource {
private static final Logger LOGGER = LoggerFactory.getLogger(PinotTenantRestletResource.class);
private static final String TENANT_NAME = "tenantName";
private final ObjectMapper _objectMapper;
public PinotTenantRestletResource() {
getVariants().add(new Variant(MediaType.TEXT_PLAIN));
getVariants().add(new Variant(MediaType.APPLICATION_JSON));
setNegotiated(false);
_objectMapper = new ObjectMapper();
}
/*
* For tenant creation
*/
@Override
@Post("json")
public Representation post(Representation entity) {
StringRepresentation presentation;
try {
final Tenant tenant = _objectMapper.readValue(entity.getText(), Tenant.class);
presentation = createTenant(tenant);
} catch (final Exception e) {
presentation = exceptionToStringRepresentation(e);
LOGGER.error("Caught exception while creating tenant ", e);
ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_TABLE_TENANT_CREATE_ERROR, 1L);
setStatus(Status.SERVER_ERROR_INTERNAL);
}
return presentation;
}
@HttpVerb("post")
@Summary("Creates a tenant")
@Tags({ "tenant" })
@Paths({ "/tenants", "/tenants/" })
private StringRepresentation createTenant(Tenant tenant) {
PinotResourceManagerResponse response;
StringRepresentation presentation;
switch (tenant.getTenantRole()) {
case BROKER:
response = _pinotHelixResourceManager.createBrokerTenant(tenant);
presentation = new StringRepresentation(response.toString());
break;
case SERVER:
response = _pinotHelixResourceManager.createServerTenant(tenant);
presentation = new StringRepresentation(response.toString());
break;
default:
throw new RuntimeException("Not a valid tenant creation call");
}
return presentation;
}
/*
* For tenant update
*/
@Override
@Put("json")
public Representation put(Representation entity) {
StringRepresentation presentation;
try {
final Tenant tenant = _objectMapper.readValue(ByteStreams.toByteArray(entity.getStream()), Tenant.class);
presentation = updateTenant(tenant);
} catch (final Exception e) {
presentation = exceptionToStringRepresentation(e);
LOGGER.error("Caught exception while updating tenant ", e);
ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_TABLE_TENANT_UPDATE_ERROR, 1L);
setStatus(Status.SERVER_ERROR_INTERNAL);
}
return presentation;
}
@HttpVerb("put")
@Summary("Updates a tenant")
@Tags({ "tenant" })
@Paths({ "/tenants", "/tenants/" })
private StringRepresentation updateTenant(Tenant tenant) {
PinotResourceManagerResponse response;
StringRepresentation presentation;
switch (tenant.getTenantRole()) {
case BROKER:
response = _pinotHelixResourceManager.updateBrokerTenant(tenant);
presentation = new StringRepresentation(response.toString());
break;
case SERVER:
response = _pinotHelixResourceManager.updateServerTenant(tenant);
presentation = new StringRepresentation(response.toString());
break;
default:
throw new RuntimeException("Not a valid tenant update call");
}
return presentation;
}
/**
* URI Mappings:
* "/tenants", "/tenants/": List all the tenants in the cluster.
* "/tenants/{tenantName}", "/tenants/{tenantName}/": List all instances for the tenant.
* "/tenants/{tenantName}?state={state}":
* - Set the state for the specified tenant to the specified value, one of {enable|disable|drop}.
*
* {@inheritDoc}
* @see org.restlet.resource.ServerResource#get()
*/
@Override
@Get
public Representation get() {
StringRepresentation presentation = null;
try {
final String tenantName = (String) getRequest().getAttributes().get(TENANT_NAME);
final String state = getReference().getQueryAsForm().getValues(STATE);
;
final String type = getReference().getQueryAsForm().getValues(TABLE_TYPE);
if (tenantName == null) {
presentation = getAllTenants(type);
} else if (state == null) {
presentation = getTenant(tenantName, type);
} else {
if (isValidState(state)) {
presentation = toggleTenantState(tenantName, state, type);
} else {
LOGGER.error(INVALID_STATE_ERROR);
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return new StringRepresentation(INVALID_STATE_ERROR);
}
}
} catch (final Exception e) {
presentation = exceptionToStringRepresentation(e);
ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_TABLE_TENANT_GET_ERROR, 1L);
setStatus(Status.SERVER_ERROR_INTERNAL);
LOGGER.error("Caught exception while fetching tenant ", e);
setStatus(Status.SERVER_ERROR_INTERNAL);
}
return presentation;
}
@HttpVerb("get")
@Summary("Gets information about a tenant")
@Tags({ "tenant" })
@Paths({ "/tenants/{tenantName}/metadata", "/tenants/{tenantName}/metadata/" })
private StringRepresentation getTenant(
@Parameter(name = "tenantName", in = "path", description = "The tenant name") String tenantName, @Parameter(
name = "type", in = "query", description = "The type of tenant, either SERVER or BROKER") String type)
throws JSONException {
StringRepresentation presentation;// Return instances related to given tenant name.
JSONObject resourceGetRet = new JSONObject();
if (type == null) {
resourceGetRet.put("ServerInstances", _pinotHelixResourceManager.getAllInstancesForServerTenant(tenantName));
resourceGetRet.put("BrokerInstances", _pinotHelixResourceManager.getAllInstancesForBrokerTenant(tenantName));
} else {
if (type.equalsIgnoreCase("server")) {
resourceGetRet.put("ServerInstances", _pinotHelixResourceManager.getAllInstancesForServerTenant(tenantName));
}
if (type.equalsIgnoreCase("broker")) {
resourceGetRet.put("BrokerInstances", _pinotHelixResourceManager.getAllInstancesForBrokerTenant(tenantName));
}
}
resourceGetRet.put(TENANT_NAME, tenantName);
presentation = new StringRepresentation(resourceGetRet.toString(), MediaType.APPLICATION_JSON);
return presentation;
}
@HttpVerb("get")
@Summary("Enable, disable or drop a tenant")
@Tags({ "tenant" })
@Paths({ "/tenants/{tenantName}", "/tenants/{tenantName}/" })
private StringRepresentation toggleTenantState(
@Parameter(name = "tenantName", in = "path", description = "The tenant name")
String tenantName,
@Parameter(name = "state", in = "query", description = "state to set for the tenant {enable|disable|drop}")
String state,
@Parameter(name = "type", in = "query", description = "The type of tenant, either SERVER or BROKER or NULL")
String type) throws JSONException {
Set<String> serverInstances = new HashSet<String>();
Set<String> brokerInstances = new HashSet<String>();
JSONObject instanceResult = new JSONObject();
if ((type == null) || type.equalsIgnoreCase("server")) {
serverInstances = _pinotHelixResourceManager.getAllInstancesForServerTenant(tenantName);
}
if ((type == null) || type.equalsIgnoreCase("broker")) {
brokerInstances = _pinotHelixResourceManager.getAllInstancesForBrokerTenant(tenantName);
}
Set<String> allInstances = new HashSet<String>(serverInstances);
allInstances.addAll(brokerInstances);
if (StateType.DROP.name().equalsIgnoreCase(state)) {
if (!allInstances.isEmpty()) {
setStatus(Status.CLIENT_ERROR_BAD_REQUEST);
return new StringRepresentation("Error: Tenant " + tenantName + " has live instances, cannot be dropped.");
}
_pinotHelixResourceManager.deleteBrokerTenantFor(tenantName);
_pinotHelixResourceManager.deleteOfflineServerTenantFor(tenantName);
_pinotHelixResourceManager.deleteRealtimeServerTenantFor(tenantName);
return new StringRepresentation("Dropped tenant " + tenantName + " successfully.");
}
boolean enable = StateType.ENABLE.name().equalsIgnoreCase(state) ? true : false;
for (String instance : allInstances) {
if (enable) {
instanceResult.put(instance, _pinotHelixResourceManager.enableInstance(instance));
} else {
instanceResult.put(instance, _pinotHelixResourceManager.disableInstance(instance));
}
}
return new StringRepresentation(instanceResult.toString(), MediaType.APPLICATION_JSON);
}
@HttpVerb("get")
@Summary("Gets information about all tenants")
@Tags({ "tenant" })
@Paths({ "/tenants", "/tenants/" })
private StringRepresentation getAllTenants(@Parameter(name = "type", in = "query",
description = "The type of tenant, either SERVER or BROKER") String type) throws JSONException {
StringRepresentation presentation;// Return all the tags.
final JSONObject ret = new JSONObject();
if (type == null || type.equalsIgnoreCase("server")) {
ret.put("SERVER_TENANTS", _pinotHelixResourceManager.getAllServerTenantNames());
}
if (type == null || type.equalsIgnoreCase("broker")) {
ret.put("BROKER_TENANTS", _pinotHelixResourceManager.getAllBrokerTenantNames());
}
presentation = new StringRepresentation(ret.toString(), MediaType.APPLICATION_JSON);
return presentation;
}
@Override
@Delete
public Representation delete() {
StringRepresentation presentation;
try {
final String tenantName = (String) getRequest().getAttributes().get(TENANT_NAME);
final String type = getReference().getQueryAsForm().getValues("type");
presentation = deleteTenant(tenantName, type);
} catch (final Exception e) {
presentation = exceptionToStringRepresentation(e);
LOGGER.error("Caught exception while deleting tenant ", e);
ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_TABLE_TENANT_DELETE_ERROR, 1L);
setStatus(Status.SERVER_ERROR_INTERNAL);
}
return presentation;
}
@HttpVerb("delete")
@Summary("Deletes a tenant")
@Tags({ "tenant" })
@Description("Deletes a tenant from the cluster")
@Paths({ "/tenants/{tenantName}", "/tenants/{tenantName}/" })
private StringRepresentation deleteTenant(
@Parameter(name = "tenantName", in = "path", description = "The tenant id") String tenantName,
@Parameter(name = "type", in = "query", description = "The type of tenant, either SERVER or BROKER",
required = true) String type) {
StringRepresentation presentation;
if (type == null) {
presentation =
new StringRepresentation("Not specify the type for the tenant name. Please try to append:"
+ "/?type=SERVER or /?type=BROKER ");
} else {
TenantRole tenantRole = TenantRole.valueOf(type.toUpperCase());
PinotResourceManagerResponse res = null;
switch (tenantRole) {
case BROKER:
if (_pinotHelixResourceManager.isBrokerTenantDeletable(tenantName)) {
res = _pinotHelixResourceManager.deleteBrokerTenantFor(tenantName);
} else {
res = new PinotResourceManagerResponse();
res.status = ResponseStatus.failure;
res.message = "Broker Tenant is not null, cannot delete it.";
}
break;
case SERVER:
if (_pinotHelixResourceManager.isServerTenantDeletable(tenantName)) {
res = _pinotHelixResourceManager.deleteOfflineServerTenantFor(tenantName);
if (res.isSuccessful()) {
res = _pinotHelixResourceManager.deleteRealtimeServerTenantFor(tenantName);
}
} else {
res = new PinotResourceManagerResponse();
res.status = ResponseStatus.failure;
res.message = "Server Tenant is not null, cannot delete it.";
}
break;
default:
break;
}
presentation = new StringRepresentation(res.toString());
}
return presentation;
}
}