/** * Copyright 2016 Yahoo Inc. * * 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.yahoo.pulsar.broker.admin; import java.util.Collections; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import com.yahoo.pulsar.common.naming.NamedEntity; import com.yahoo.pulsar.common.policies.data.PropertyAdmin; import com.yahoo.pulsar.broker.web.RestException; @Path("/properties") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Api(value = "/properties", description = "Properties admin apis", tags = "properties") public class Properties extends AdminResource { @GET @ApiOperation(value = "Get the list of properties.", response = String.class, responseContainer = "List") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Property doesn't exist") }) public List<String> getProperties() { validateSuperUserAccess(); try { List<String> properties = globalZk().getChildren(path("policies"), false); properties.sort(null); return properties; } catch (Exception e) { log.error("[{}] Failed to get properties list", clientAppId(), e); throw new RestException(e); } } @GET @Path("/{property}") @ApiOperation(value = "Get the admin configuration for a given property.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Property doesn't exist") }) public PropertyAdmin getPropertyAdmin(@PathParam("property") String property) { validateSuperUserAccess(); try { return propertiesCache().get(path("policies", property)) .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Property does not exist")); } catch (Exception e) { log.error("[{}] Failed to get property {}", clientAppId(), property, e); throw new RestException(e); } } @PUT @Path("/{property}") @ApiOperation(value = "Create a new property.", notes = "This operation requires Pulsar super-user privileges.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 409, message = "Property already exist"), @ApiResponse(code = 412, message = "Property name is not valid") }) public void createProperty(@PathParam("property") String property, PropertyAdmin config) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); try { NamedEntity.checkName(property); zkCreate(path("policies", property), jsonMapper().writeValueAsBytes(config)); log.info("[{}] Created property {}", clientAppId(), property); } catch (KeeperException.NodeExistsException e) { log.warn("[{}] Failed to create already existing property {}", clientAppId(), property); throw new RestException(Status.CONFLICT, "Property already exist"); } catch (IllegalArgumentException e) { log.warn("[{}] Failed to create property with invalid name {}", clientAppId(), property, e); throw new RestException(Status.PRECONDITION_FAILED, "Property name is not valid"); } catch (Exception e) { log.error("[{}] Failed to create property {}", clientAppId(), property, e); throw new RestException(e); } } @POST @Path("/{property}") @ApiOperation(value = "Update the admins for a property.", notes = "This operation requires Pulsar super-user privileges.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Property does not exist"), @ApiResponse(code = 409, message = "Property already exist") }) public void updateProperty(@PathParam("property") String property, PropertyAdmin newPropertyAdmin) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); Stat nodeStat = new Stat(); try { byte[] content = globalZk().getData(path("policies", property), null, nodeStat); PropertyAdmin oldPropertyAdmin = jsonMapper().readValue(content, PropertyAdmin.class); List<String> clustersWithActiveNamespaces = Lists.newArrayList(); if (oldPropertyAdmin.getAllowedClusters().size() > newPropertyAdmin.getAllowedClusters().size()) { // Get the colo(s) being removed from the list oldPropertyAdmin.getAllowedClusters().removeAll(newPropertyAdmin.getAllowedClusters()); log.debug("Following clusters are being removed : [{}]", oldPropertyAdmin.getAllowedClusters()); for (String cluster : oldPropertyAdmin.getAllowedClusters()) { List<String> activeNamespaces = Lists.newArrayList(); try { activeNamespaces = globalZk().getChildren(path("policies", property, cluster), false); if (activeNamespaces.size() != 0) { // There are active namespaces in this cluster clustersWithActiveNamespaces.add(cluster); } } catch (KeeperException.NoNodeException nne) { // Fine, some cluster does not have active namespace. Move on! } } if (!clustersWithActiveNamespaces.isEmpty()) { // Throw an exception because colos being removed are having active namespaces String msg = String.format( "Failed to update the property because active namespaces are present in colos %s. Please delete those namespaces first", clustersWithActiveNamespaces); throw new RestException(Status.CONFLICT, msg); } } String propertyPath = path("policies", property); globalZk().setData(propertyPath, jsonMapper().writeValueAsBytes(newPropertyAdmin), -1); globalZkCache().invalidate(propertyPath); log.info("[{}] updated property {}", clientAppId(), property); } catch (RestException re) { throw re; } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to update property {}: does not exist", clientAppId(), property); throw new RestException(Status.NOT_FOUND, "Property does not exist"); } catch (Exception e) { log.error("[{}] Failed to update property {}", clientAppId(), property, e); throw new RestException(e); } } @DELETE @Path("/{property}") @ApiOperation(value = "elete a property and all namespaces and destinations under it.") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Property does not exist"), @ApiResponse(code = 409, message = "The property still has active namespaces") }) public void deleteProperty(@PathParam("property") String property) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); boolean isPropertyEmpty = false; try { isPropertyEmpty = getListOfNamespaces(property).isEmpty(); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to delete property {}: does not exist", clientAppId(), property); throw new RestException(Status.NOT_FOUND, "The property does not exist"); } catch (Exception e) { log.error("[{}] Failed to get property status {}", clientAppId(), property, e); throw new RestException(e); } if (!isPropertyEmpty) { log.warn("[{}] Failed to delete property {}: not empty", clientAppId(), property); throw new RestException(Status.CONFLICT, "The property still has active namespaces"); } try { // First try to delete every cluster z-node for (String cluster : globalZk().getChildren(path("policies", property), false)) { globalZk().delete(path("policies", property, cluster), -1); } globalZk().delete(path("policies", property), -1); log.info("[{}] Deleted property {}", clientAppId(), property); } catch (Exception e) { log.error("[{}] Failed to delete property {}", clientAppId(), property, e); throw new RestException(e); } } private static final Logger log = LoggerFactory.getLogger(Properties.class); }