blob: 00e4e28744eb2dbe1f28bacfba9203fb9b55d797 [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.knox.gateway.service.admin;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.knox.gateway.i18n.GatewaySpiMessages;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.service.admin.beans.BeanConverter;
import org.apache.knox.gateway.service.admin.beans.Topology;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.topology.TopologyService;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.notModified;
import static javax.ws.rs.core.Response.status;
@Path("/api/v1")
public class TopologiesResource {
private static final String XML_EXT = ".xml";
private static final String JSON_EXT = ".json";
private static final String YAML_EXT = ".yml";
private static final String TOPOLOGIES_API_PATH = "topologies";
private static final String SINGLE_TOPOLOGY_API_PATH = TOPOLOGIES_API_PATH + "/{id}";
private static final String PROVIDERCONFIG_API_PATH = "providerconfig";
private static final String SINGLE_PROVIDERCONFIG_API_PATH = PROVIDERCONFIG_API_PATH + "/{name}";
private static final String DESCRIPTORS_API_PATH = "descriptors";
private static final String SINGLE_DESCRIPTOR_API_PATH = DESCRIPTORS_API_PATH + "/{name}";
private static final int RESOURCE_NAME_LENGTH_MAX = 100;
private static final Pattern RESOURCE_NAME_PATTERN = Pattern.compile("^[\\w-/.]+$");
private static GatewaySpiMessages log = MessagesFactory.get(GatewaySpiMessages.class);
private static final Map<MediaType, String> mediaTypeFileExtensions = new HashMap<>();
static {
mediaTypeFileExtensions.put(MediaType.APPLICATION_XML_TYPE, XML_EXT);
mediaTypeFileExtensions.put(MediaType.APPLICATION_JSON_TYPE, JSON_EXT);
mediaTypeFileExtensions.put(MediaType.TEXT_PLAIN_TYPE, YAML_EXT);
}
@Context
private HttpServletRequest request;
@GET
@Produces({APPLICATION_JSON, APPLICATION_XML})
@Path(SINGLE_TOPOLOGY_API_PATH)
public Topology getTopology(@PathParam("id") String id) {
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
if (services != null) {
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
GatewayConfig config =
(GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
if (t.getName().equals(id)) {
// we need to convert first so that the original topology does not get
// overwritten in TopologyService (i.e. URI does not change from 'file://...' to
// 'https://...'
final Topology convertedTopology = BeanConverter.getTopology(t);
try {
convertedTopology.setUri(new URI( buildURI(t, config, request) ));
} catch (URISyntaxException se) {
convertedTopology.setUri(null);
}
// For any read-only override topology, mark it as generated to discourage modification.
final List<String> ambariManagedTopos = config.getReadOnlyOverrideTopologyNames();
if (ambariManagedTopos.contains(convertedTopology.getName())) {
convertedTopology.setGenerated(true);
}
return convertedTopology;
}
}
}
return null;
}
@GET
@Produces({APPLICATION_JSON, APPLICATION_XML})
@Path(TOPOLOGIES_API_PATH)
public SimpleTopologyWrapper getTopologies() {
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
GatewayConfig config =
(GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
ArrayList<SimpleTopology> st = new ArrayList<>();
for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
st.add(getSimpleTopology(t, config));
}
st.sort(new TopologyComparator());
SimpleTopologyWrapper stw = new SimpleTopologyWrapper();
stw.topologies.addAll(st);
return stw;
}
@PUT
@Consumes({APPLICATION_JSON, APPLICATION_XML})
@Path(SINGLE_TOPOLOGY_API_PATH)
public Topology uploadTopology(@PathParam("id") String id, Topology t) {
Topology result = null;
try {
id = URLDecoder.decode(id, StandardCharsets.UTF_8.name());
} catch (Exception e) {
// Ignore
}
if (!isValidResourceName(id)) {
log.invalidResourceName(id);
throw new BadRequestException("Invalid topology name: " + id);
}
GatewayServices gs =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
t.setName(id);
TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);
// Check for existing topology with the same name, to see if it had been generated
boolean existingGenerated = false;
for (org.apache.knox.gateway.topology.Topology existingTopology : ts.getTopologies()) {
if(existingTopology.getName().equals(id)) {
existingGenerated = existingTopology.isGenerated();
break;
}
}
// If a topology with the same ID exists, which had been generated, then DO NOT overwrite it because it will be
// out of sync with the source descriptor. Otherwise, deploy the updated version.
if (!existingGenerated) {
ts.deployTopology(BeanConverter.getTopology(t));
result = getTopology(id);
} else {
log.disallowedOverwritingGeneratedTopology(id);
}
return result;
}
@DELETE
@Produces(APPLICATION_JSON)
@Path(SINGLE_TOPOLOGY_API_PATH)
public Response deleteTopology(@PathParam("id") String id) {
boolean deleted = false;
if(!"admin".equals(id)) {
GatewayServices services = (GatewayServices) request.getServletContext()
.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
for (org.apache.knox.gateway.topology.Topology t : ts.getTopologies()) {
if(t.getName().equals(id)) {
ts.deleteTopology(t);
deleted = true;
}
}
}else{
deleted = false;
}
return ok().entity("{ \"deleted\" : " + deleted + " }").build();
}
@GET
@Produces({APPLICATION_JSON})
@Path(PROVIDERCONFIG_API_PATH)
public HrefListing getProviderConfigurations() {
HrefListing listing = new HrefListing();
listing.setHref(buildHref(request));
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
List<HrefListItem> configs = new ArrayList<>();
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
// Get all the simple descriptor file names
for (File providerConfig : ts.getProviderConfigurations()){
String id = FilenameUtils.getBaseName(providerConfig.getName());
configs.add(new HrefListItem(buildHref(id, request), providerConfig.getName()));
}
listing.setItems(configs);
return listing;
}
@GET
@Produces({APPLICATION_XML, APPLICATION_JSON, TEXT_PLAIN})
@Path(SINGLE_PROVIDERCONFIG_API_PATH)
public Response getProviderConfiguration(@PathParam("name") String name) {
Response response;
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
File providerConfigFile = null;
for (File pc : ts.getProviderConfigurations()){
// If the file name matches the specified id
if (FilenameUtils.getBaseName(pc.getName()).equals(name)) {
providerConfigFile = pc;
break;
}
}
if (providerConfigFile != null) {
byte[] content;
try {
content = FileUtils.readFileToByteArray(providerConfigFile);
response = ok().entity(content).build();
} catch (IOException e) {
log.failedToReadConfigurationFile(providerConfigFile.getAbsolutePath(), e);
response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
} else {
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
@DELETE
@Produces(APPLICATION_JSON)
@Path(SINGLE_PROVIDERCONFIG_API_PATH)
public Response deleteProviderConfiguration(@PathParam("name") String name, @QueryParam("force") String force) {
Response response;
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
if (ts.deleteProviderConfiguration(name, Boolean.valueOf(force))) {
response = ok().entity("{ \"deleted\" : \"provider config " + name + "\" }").build();
} else {
response = notModified().build();
}
return response;
}
@DELETE
@Produces(APPLICATION_JSON)
@Path(SINGLE_DESCRIPTOR_API_PATH)
public Response deleteSimpleDescriptor(@PathParam("name") String name) {
Response response = null;
if(!"admin".equals(name)) {
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
if (ts.deleteDescriptor(name)) {
response = ok().entity("{ \"deleted\" : \"descriptor " + name + "\" }").build();
}
}
if (response == null) {
response = notModified().build();
}
return response;
}
@PUT
@Consumes({APPLICATION_XML, APPLICATION_JSON, TEXT_PLAIN})
@Path(SINGLE_PROVIDERCONFIG_API_PATH)
public Response uploadProviderConfiguration(@PathParam("name") String name, @Context HttpHeaders headers, String content) {
Response response = null;
try {
name = URLDecoder.decode(name, StandardCharsets.UTF_8.name());
} catch (Exception e) {
// Ignore
}
if (!isValidResourceName(name)) {
log.invalidResourceName(name);
throw new BadRequestException("Invalid provider configuration name: " + name);
}
GatewayServices gs =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);
File existing = getExistingConfigFile(ts.getProviderConfigurations(), name);
boolean isUpdate = (existing != null);
// If it's an update, then use the matching existing filename; otherwise, use the media type to determine the file
// extension.
String filename = isUpdate ? existing.getName() : getFileNameForResource(name, headers);
if (ts.deployProviderConfiguration(filename, content)) {
try {
if (isUpdate) {
response = Response.noContent().build();
} else {
response = created(new URI(buildHref(request))).build();
}
} catch (URISyntaxException e) {
log.invalidResourceURI(e.getInput(), e.getReason(), e);
response = status(Response.Status.BAD_REQUEST).entity("{ \"error\" : \"Failed to deploy provider configuration " + name + "\" }").build();
}
}
return response;
}
@PUT
@Consumes({APPLICATION_JSON, TEXT_PLAIN})
@Path(SINGLE_DESCRIPTOR_API_PATH)
public Response uploadSimpleDescriptor(@PathParam("name") String name,
@Context HttpHeaders headers,
String content) {
Response response = null;
try {
name = URLDecoder.decode(name, StandardCharsets.UTF_8.name());
} catch (Exception e) {
// Ignore
}
if (!isValidResourceName(name)) {
log.invalidResourceName(name);
throw new BadRequestException("Invalid descriptor name: " + name);
}
GatewayServices gs =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = gs.getService(ServiceType.TOPOLOGY_SERVICE);
File existing = getExistingConfigFile(ts.getDescriptors(), name);
boolean isUpdate = (existing != null);
// If it's an update, then use the matching existing filename; otherwise, use the media type to determine the file
// extension.
String filename = isUpdate ? existing.getName() : getFileNameForResource(name, headers);
if (ts.deployDescriptor(filename, content)) {
try {
if (isUpdate) {
response = Response.noContent().build();
} else {
response = created(new URI(buildHref(request))).build();
}
} catch (URISyntaxException e) {
log.invalidResourceURI(e.getInput(), e.getReason(), e);
response = status(Response.Status.BAD_REQUEST).entity("{ \"error\" : \"Failed to deploy descriptor " + name + "\" }").build();
}
}
return response;
}
@GET
@Produces({APPLICATION_JSON})
@Path(DESCRIPTORS_API_PATH)
public HrefListing getSimpleDescriptors() {
HrefListing listing = new HrefListing();
listing.setHref(buildHref(request));
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
List<HrefListItem> descriptors = new ArrayList<>();
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
for (File descriptor : ts.getDescriptors()){
String id = FilenameUtils.getBaseName(descriptor.getName());
descriptors.add(new HrefListItem(buildHref(id, request), descriptor.getName()));
}
listing.setItems(descriptors);
return listing;
}
@GET
@Produces({APPLICATION_JSON, TEXT_PLAIN})
@Path(SINGLE_DESCRIPTOR_API_PATH)
public Response getSimpleDescriptor(@PathParam("name") String name) {
Response response;
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
TopologyService ts = services.getService(ServiceType.TOPOLOGY_SERVICE);
File descriptorFile = null;
for (File sd : ts.getDescriptors()){
// If the file name matches the specified id
if (FilenameUtils.getBaseName(sd.getName()).equals(name)) {
descriptorFile = sd;
break;
}
}
if (descriptorFile != null) {
String mediaType = APPLICATION_JSON;
byte[] content;
try {
if ("yml".equals(FilenameUtils.getExtension(descriptorFile.getName()))) {
mediaType = TEXT_PLAIN;
}
content = FileUtils.readFileToByteArray(descriptorFile);
response = ok().type(mediaType).entity(content).build();
} catch (IOException e) {
log.failedToReadConfigurationFile(descriptorFile.getAbsolutePath(), e);
response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
} else {
response = Response.status(Response.Status.NOT_FOUND).build();
}
return response;
}
private String getFileNameForResource(String resourceName, HttpHeaders headers) {
String filename;
String extension = FilenameUtils.getExtension(resourceName);
if (extension != null && !extension.isEmpty()) {
filename = resourceName;
} else {
extension = getExtensionForMediaType(headers.getMediaType());
filename = (extension != null) ? (resourceName + extension) : (resourceName + JSON_EXT);
}
return filename;
}
private String getExtensionForMediaType(MediaType type) {
String extension = null;
for (Map.Entry<MediaType, String> entry : mediaTypeFileExtensions.entrySet()) {
if (type.isCompatible(entry.getKey())) {
extension = entry.getValue();
break;
}
}
return extension;
}
private File getExistingConfigFile(Collection<File> existing, String candidateName) {
File result = null;
for (File exists : existing) {
if (FilenameUtils.getBaseName(exists.getName()).equals(candidateName)) {
result = exists;
break;
}
}
return result;
}
private static boolean isValidResourceName(final String name) {
return name != null && name.length() <= RESOURCE_NAME_LENGTH_MAX &&
RESOURCE_NAME_PATTERN.matcher(name).matches();
}
private static class TopologyComparator implements Comparator<SimpleTopology> {
@Override
public int compare(SimpleTopology t1, SimpleTopology t2) {
return t1.getName().compareTo(t2.getName());
}
}
String buildURI(org.apache.knox.gateway.topology.Topology topology, GatewayConfig config, HttpServletRequest req){
String uri = buildXForwardBaseURL(req);
// Strip extra context
uri = uri.replace(req.getContextPath(), "");
// Add the gateway path
String gatewayPath;
if(config.getGatewayPath() != null){
gatewayPath = config.getGatewayPath();
}else{
gatewayPath = "gateway";
}
return uri + "/" + gatewayPath + "/" + topology.getName();
}
String buildHref(HttpServletRequest req) {
return buildHref((String)null, req);
}
String buildHref(String id, HttpServletRequest req) {
StringBuilder href = new StringBuilder(buildXForwardBaseURL(req));
// Make sure that the pathInfo doesn't have any '/' chars at the end.
String pathInfo = req.getPathInfo();
while(pathInfo.endsWith("/")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
}
href.append(pathInfo);
if (id != null) {
href.append('/').append(id);
}
return href.toString();
}
String buildHref(org.apache.knox.gateway.topology.Topology t, HttpServletRequest req) {
return buildHref(t.getName(), req);
}
private SimpleTopology getSimpleTopology(org.apache.knox.gateway.topology.Topology t, GatewayConfig config) {
String uri = buildURI(t, config, request);
String href = buildHref(t, request);
return new SimpleTopology(t, uri, href);
}
private String buildXForwardBaseURL(HttpServletRequest req){
final String X_Forwarded = "X-Forwarded-";
final String X_Forwarded_Context = X_Forwarded + "Context";
final String X_Forwarded_Proto = X_Forwarded + "Proto";
final String X_Forwarded_Host = X_Forwarded + "Host";
final String X_Forwarded_Port = X_Forwarded + "Port";
final String X_Forwarded_Server = X_Forwarded + "Server";
StringBuilder baseURL = new StringBuilder();
// Get Protocol
if(req.getHeader(X_Forwarded_Proto) != null){
baseURL.append(req.getHeader(X_Forwarded_Proto)).append("://");
} else {
baseURL.append(req.getProtocol()).append("://");
}
// Handle Server/Host and Port Here
if (req.getHeader(X_Forwarded_Host) != null && req.getHeader(X_Forwarded_Port) != null){
// Double check to see if host has port
if(req.getHeader(X_Forwarded_Host).contains(req.getHeader(X_Forwarded_Port))){
baseURL.append(req.getHeader(X_Forwarded_Host));
} else {
// If there's no port, add the host and port together;
baseURL.append(req.getHeader(X_Forwarded_Host)).append(':').append(req.getHeader(X_Forwarded_Port));
}
} else if(req.getHeader(X_Forwarded_Server) != null && req.getHeader(X_Forwarded_Port) != null){
// Tack on the server and port if they're available. Try host if server not available
baseURL.append(req.getHeader(X_Forwarded_Server)).append(':').append(req.getHeader(X_Forwarded_Port));
} else if(req.getHeader(X_Forwarded_Port) != null) {
// if we at least have a port, we can use it.
baseURL.append(req.getServerName()).append(':').append(req.getHeader(X_Forwarded_Port));
} else {
// Resort to request members
baseURL.append(req.getServerName()).append(':').append(req.getLocalPort());
}
// Handle Server context
if( req.getHeader(X_Forwarded_Context) != null ) {
baseURL.append(req.getHeader( X_Forwarded_Context ));
} else {
baseURL.append(req.getContextPath());
}
return baseURL.toString();
}
static class HrefListing {
@JsonProperty
String href;
@JsonProperty
List<HrefListItem> items;
HrefListing() {}
public void setHref(String href) {
this.href = href;
}
public String getHref() {
return href;
}
public void setItems(List<HrefListItem> items) {
this.items = items;
}
public List<HrefListItem> getItems() {
return items;
}
}
static class HrefListItem {
@JsonProperty
String href;
@JsonProperty
String name;
HrefListItem() {}
HrefListItem(String href, String name) {
this.href = href;
this.name = name;
}
public void setHref(String href) {
this.href = href;
}
public String getHref() {
return href;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@XmlAccessorType(XmlAccessType.NONE)
public static class SimpleTopology {
@XmlElement
private String name;
@XmlElement
private String timestamp;
@XmlElement
private String defaultServicePath;
@XmlElement
private String uri;
@XmlElement
private String href;
public SimpleTopology() {}
public SimpleTopology(org.apache.knox.gateway.topology.Topology t, String uri, String href) {
this.name = t.getName();
this.timestamp = Long.toString(t.getTimestamp());
this.defaultServicePath = t.getDefaultServicePath();
this.uri = uri;
this.href = href;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
public String getTimestamp() {
return timestamp;
}
public void setDefaultService(String defaultServicePath) {
this.defaultServicePath = defaultServicePath;
}
public String getDefaultService() {
return defaultServicePath;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class SimpleTopologyWrapper{
@XmlElement(name="topology")
@XmlElementWrapper(name="topologies")
private List<SimpleTopology> topologies = new ArrayList<>();
public List<SimpleTopology> getTopologies(){
return topologies;
}
public void setTopologies(List<SimpleTopology> ts){
this.topologies = ts;
}
}
}