/** * 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.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; 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.bookkeeper.util.ZkUtils; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs.Ids; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.databind.JsonMappingException; import com.yahoo.pulsar.broker.web.RestException; import com.yahoo.pulsar.common.naming.NamedEntity; import com.yahoo.pulsar.common.policies.data.ClusterData; import com.yahoo.pulsar.common.policies.data.NamespaceIsolationData; import com.yahoo.pulsar.common.policies.impl.NamespaceIsolationPolicies; import com.yahoo.pulsar.common.util.ObjectMapperFactory; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; @Path("/clusters") @Api(value = "/clusters", description = "Cluster admin apis", tags = "clusters") @Produces(MediaType.APPLICATION_JSON) public class Clusters extends AdminResource { @GET @ApiOperation(value = "Get the list of all the Pulsar clusters.", response = String.class, responseContainer = "Set") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission") }) public Set<String> getClusters() throws Exception { try { return clustersListCache().get(); } catch (Exception e) { log.error("[{}] Failed to get clusters list", clientAppId(), e); throw new RestException(e); } } @GET @Path("/{cluster}") @ApiOperation(value = "Get the configuration data for the specified cluster.", response = ClusterData.class) @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Cluster doesn't exist") }) public ClusterData getCluster(@PathParam("cluster") String cluster) { validateSuperUserAccess(); try { return clustersCache().get(path("clusters", cluster)) .orElseThrow(() -> new RestException(Status.NOT_FOUND, "Cluster does not exist")); } catch (Exception e) { log.error("[{}] Failed to get cluster {}", clientAppId(), cluster, e); if (e instanceof RestException) { throw (RestException) e; } else { throw new RestException(e); } } } @PUT @Path("/{cluster}") @ApiOperation(value = "Provisions a new cluster. This operation requires Pulsar super-user privileges.", notes = "The name cannot contain '/' characters.") @ApiResponses(value = { @ApiResponse(code = 204, message = "Cluster has been created"), @ApiResponse(code = 403, message = "You don't have admin permission to create the cluster"), @ApiResponse(code = 409, message = "Cluster already exists"), @ApiResponse(code = 412, message = "Cluster name is not valid") }) public void createCluster(@PathParam("cluster") String cluster, ClusterData clusterData) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); try { NamedEntity.checkName(cluster); zkCreate(path("clusters", cluster), jsonMapper().writeValueAsBytes(clusterData)); log.info("[{}] Created cluster {}", clientAppId(), cluster); } catch (KeeperException.NodeExistsException e) { log.warn("[{}] Failed to create already existing cluster {}", clientAppId(), cluster); throw new RestException(Status.CONFLICT, "Cluster already exist"); } catch (IllegalArgumentException e) { log.warn("[{}] Failed to create cluster with invalid name {}", clientAppId(), cluster, e); throw new RestException(Status.PRECONDITION_FAILED, "Cluster name is not valid"); } catch (Exception e) { log.error("[{}] Failed to create cluster {}", clientAppId(), cluster, e); throw new RestException(e); } } @POST @Path("/{cluster}") @ApiOperation(value = "Update the configuration for a cluster.", notes = "This operation requires Pulsar super-user privileges.") @ApiResponses(value = { @ApiResponse(code = 204, message = "Cluster has been updated"), @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Cluster doesn't exist") }) public void updateCluster(@PathParam("cluster") String cluster, ClusterData clusterData) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); try { String clusterPath = path("clusters", cluster); globalZk().setData(clusterPath, jsonMapper().writeValueAsBytes(clusterData), -1); globalZkCache().invalidate(clusterPath); log.info("[{}] Updated cluster {}", clientAppId(), cluster); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to update cluster {}: Does not exist", clientAppId(), cluster); throw new RestException(Status.NOT_FOUND, "Cluster does not exist"); } catch (Exception e) { log.error("[{}] Failed to update cluster {}", clientAppId(), cluster, e); throw new RestException(e); } } @DELETE @Path("/{cluster}") @ApiOperation(value = "Delete an existing cluster") @ApiResponses(value = { @ApiResponse(code = 204, message = "Cluster has been updated"), @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Cluster doesn't exist"), @ApiResponse(code = 412, message = "Cluster is not empty") }) public void deleteCluster(@PathParam("cluster") String cluster) { validateSuperUserAccess(); validatePoliciesReadOnlyAccess(); // Check that the cluster is not used by any property (eg: no namespaces provisioned there) boolean isClusterUsed = false; try { for (String property : globalZk().getChildren(path("policies"), false)) { if (globalZk().exists(path("policies", property, cluster), false) == null) { continue; } if (!globalZk().getChildren(path("policies", property, cluster), false).isEmpty()) { // We found a property that has at least a namespace in this cluster isClusterUsed = true; break; } } // check the namespaceIsolationPolicies associated with the cluster String path = path("clusters", cluster, "namespaceIsolationPolicies"); Optional<NamespaceIsolationPolicies> nsIsolationPolicies = namespaceIsolationPoliciesCache().get(path); // Need to delete the isolation policies if present if (nsIsolationPolicies.isPresent()) { if (nsIsolationPolicies.get().getPolicies().isEmpty()) { globalZk().delete(path, -1); namespaceIsolationPoliciesCache().invalidate(path); } else { isClusterUsed = true; } } } catch (Exception e) { log.error("[{}] Failed to get cluster usage {}", clientAppId(), cluster, e); throw new RestException(e); } if (isClusterUsed) { log.warn("[{}] Failed to delete cluster {} - Cluster not empty", clientAppId(), cluster); throw new RestException(Status.PRECONDITION_FAILED, "Cluster not empty"); } try { String clusterPath = path("clusters", cluster); globalZk().delete(clusterPath, -1); globalZkCache().invalidate(clusterPath); log.info("[{}] Deleted cluster {}", clientAppId(), cluster); } catch (KeeperException.NoNodeException e) { log.warn("[{}] Failed to delete cluster {} - Does not exist", clientAppId(), cluster); throw new RestException(Status.NOT_FOUND, "Cluster does not exist"); } catch (Exception e) { log.error("[{}] Failed to delete cluster {}", clientAppId(), cluster, e); throw new RestException(e); } } @GET @Path("/{cluster}/namespaceIsolationPolicies") @ApiOperation(value = "Get the namespace isolation policies assigned in the cluster", response = NamespaceIsolationData.class, responseContainer = "Map") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Cluster doesn't exist") }) public Map<String, NamespaceIsolationData> getNamespaceIsolationPolicies(@PathParam("cluster") String cluster) throws Exception { validateSuperUserAccess(); if (!clustersCache().get(path("clusters", cluster)).isPresent()) { throw new RestException(Status.NOT_FOUND, "Cluster " + cluster + " does not exist."); } try { NamespaceIsolationPolicies nsIsolationPolicies = namespaceIsolationPoliciesCache() .get(path("clusters", cluster, "namespaceIsolationPolicies")) .orElseThrow(() -> new RestException(Status.NOT_FOUND, "NamespaceIsolationPolicies for cluster " + cluster + " does not exist")); // construct the response to NamespaceisolationData map return nsIsolationPolicies.getPolicies(); } catch (Exception e) { log.error("[{}] Failed to get clusters/{}/namespaceIsolationPolicies", clientAppId(), cluster, e); throw new RestException(e); } } private void validateClusterExists(String cluster) { try { if (!clustersCache().get(path("clusters", cluster)).isPresent()) { throw new RestException(Status.PRECONDITION_FAILED, "Cluster " + cluster + " does not exist."); } } catch (Exception e) { throw new RestException(e); } } @GET @Path("/{cluster}/namespaceIsolationPolicies/{policyName}") @ApiOperation(value = "Get a single namespace isolation policy assigned in the cluster", response = NamespaceIsolationData.class) @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission"), @ApiResponse(code = 404, message = "Policy doesn't exist"), @ApiResponse(code = 412, message = "Cluster doesn't exist") }) public NamespaceIsolationData getNamespaceIsolationPolicy(@PathParam("cluster") String cluster, @PathParam("policyName") String policyName) throws Exception { validateSuperUserAccess(); validateClusterExists(cluster); try { NamespaceIsolationPolicies nsIsolationPolicies = namespaceIsolationPoliciesCache() .get(path("clusters", cluster, "namespaceIsolationPolicies")) .orElseThrow(() -> new RestException(Status.NOT_FOUND, "NamespaceIsolationPolicies for cluster " + cluster + " does not exist")); // construct the response to NamespaceisolationData map if (!nsIsolationPolicies.getPolicies().containsKey(policyName)) { log.info("[{}] Cannot find NamespaceIsolationPolicy {} for cluster {}", policyName, cluster); throw new RestException(Status.NOT_FOUND, "Cannot find NamespaceIsolationPolicy " + policyName + " for cluster " + cluster); } return nsIsolationPolicies.getPolicies().get(policyName); } catch (RestException re) { throw re; } catch (Exception e) { log.error("[{}] Failed to get clusters/{}/namespaceIsolationPolicies/{}", clientAppId(), cluster, e); throw new RestException(e); } } @POST @Path("/{cluster}/namespaceIsolationPolicies/{policyName}") @ApiOperation(value = "Set namespace isolation policy") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission or plicy is read only"), @ApiResponse(code = 412, message = "Cluster doesn't exist") }) public void setNamespaceIsolationPolicy(@PathParam("cluster") String cluster, @PathParam("policyName") String policyName, NamespaceIsolationData policyData) throws Exception { validateSuperUserAccess(); validateClusterExists(cluster); validatePoliciesReadOnlyAccess(); try { // validate the policy data before creating the node policyData.validate(); String nsIsolationPolicyPath = path("clusters", cluster, "namespaceIsolationPolicies"); NamespaceIsolationPolicies nsIsolationPolicies = namespaceIsolationPoliciesCache() .get(nsIsolationPolicyPath).orElseGet(() -> { try { this.createNamespaceIsolationPolicyNode(nsIsolationPolicyPath); return new NamespaceIsolationPolicies(); } catch (KeeperException | InterruptedException e) { throw new RestException(e); } }); nsIsolationPolicies.setPolicy(policyName, policyData); globalZk().setData(nsIsolationPolicyPath, jsonMapper().writeValueAsBytes(nsIsolationPolicies.getPolicies()), -1); // make sure that the cache content will be refreshed for the next read access namespaceIsolationPoliciesCache().invalidate(nsIsolationPolicyPath); } catch (IllegalArgumentException iae) { log.info("[{}] Failed to update clusters/{}/namespaceIsolationPolicies/{}. Input data is invalid", clientAppId(), cluster, policyName, iae); String jsonInput = ObjectMapperFactory.create().writeValueAsString(policyData); throw new RestException(Status.BAD_REQUEST, "Invalid format of input policy data. policy: " + policyName + "; data: " + jsonInput); } catch (KeeperException.NoNodeException nne) { log.warn("[{}] Failed to update clusters/{}/namespaceIsolationPolicies: Does not exist", clientAppId(), cluster); throw new RestException(Status.NOT_FOUND, "NamespaceIsolationPolicies for cluster " + cluster + " does not exist"); } catch (Exception e) { log.error("[{}] Failed to update clusters/{}/namespaceIsolationPolicies/{}", clientAppId(), cluster, policyName, e); throw new RestException(e); } } private void createNamespaceIsolationPolicyNode(String nsIsolationPolicyPath) throws KeeperException, InterruptedException { // create persistent node on ZooKeeper if (globalZk().exists(nsIsolationPolicyPath, false) == null) { // create all the intermediate nodes try { ZkUtils.createFullPathOptimistic(globalZk(), nsIsolationPolicyPath, jsonMapper().writeValueAsBytes(Collections.emptyMap()), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } catch (KeeperException.NodeExistsException nee) { log.debug("Other broker preempted the full path [{}] already. Continue...", nsIsolationPolicyPath); } catch (JsonGenerationException e) { // ignore json error as it is empty hash } catch (JsonMappingException e) { } catch (IOException e) { } } } @DELETE @Path("/{cluster}/namespaceIsolationPolicies/{policyName}") @ApiOperation(value = "Delete namespace isolation policy") @ApiResponses(value = { @ApiResponse(code = 403, message = "Don't have admin permission or plicy is read only"), @ApiResponse(code = 412, message = "Cluster doesn't exist") }) public void deleteNamespaceIsolationPolicy(@PathParam("cluster") String cluster, @PathParam("policyName") String policyName) throws Exception { validateSuperUserAccess(); validateClusterExists(cluster); validatePoliciesReadOnlyAccess(); try { String nsIsolationPolicyPath = path("clusters", cluster, "namespaceIsolationPolicies"); NamespaceIsolationPolicies nsIsolationPolicies = namespaceIsolationPoliciesCache() .get(nsIsolationPolicyPath).orElseGet(() -> { try { this.createNamespaceIsolationPolicyNode(nsIsolationPolicyPath); return new NamespaceIsolationPolicies(); } catch (KeeperException | InterruptedException e) { throw new RestException(e); } }); nsIsolationPolicies.deletePolicy(policyName); globalZk().setData(nsIsolationPolicyPath, jsonMapper().writeValueAsBytes(nsIsolationPolicies.getPolicies()), -1); // make sure that the cache content will be refreshed for the next read access namespaceIsolationPoliciesCache().invalidate(nsIsolationPolicyPath); } catch (KeeperException.NoNodeException nne) { log.warn("[{}] Failed to update brokers/{}/namespaceIsolationPolicies: Does not exist", clientAppId(), cluster); throw new RestException(Status.NOT_FOUND, "NamespaceIsolationPolicies for cluster " + cluster + " does not exist"); } catch (Exception e) { log.error("[{}] Failed to update brokers/{}/namespaceIsolationPolicies/{}", clientAppId(), cluster, policyName, e); throw new RestException(e); } } private static final Logger log = LoggerFactory.getLogger(Clusters.class); }