/* * Copyright 2017 JBoss 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 io.apiman.gateway.engine.vertx.polling; import io.apiman.gateway.engine.IEngineConfig; import io.apiman.gateway.engine.Version; import io.apiman.gateway.engine.async.AsyncInitialize; import io.apiman.gateway.engine.async.AsyncResultImpl; import io.apiman.gateway.engine.async.IAsyncHandler; import io.apiman.gateway.engine.async.IAsyncResultHandler; import io.apiman.gateway.engine.beans.Api; import io.apiman.gateway.engine.beans.Client; import io.apiman.gateway.engine.beans.Policy; import io.apiman.gateway.engine.impl.InMemoryRegistry; import io.apiman.gateway.engine.vertx.polling.fetchers.AccessTokenResourceFetcher; import io.apiman.gateway.engine.vertx.polling.fetchers.FileResourceFetcher; import io.apiman.gateway.engine.vertx.polling.fetchers.HttpResourceFetcher; import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.Content; import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.ProxyConfigRoot; import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.Service; import io.apiman.gateway.engine.vertx.polling.fetchers.threescale.beans.ServicesRoot; import io.apiman.gateway.platforms.vertx3.common.verticles.Json; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.impl.Arguments; import io.vertx.core.json.DecodeException; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; /** * URI loading registry that pulls configuration from a specified 3scale backend, this is * mapped to the internal apiman data model and * <ul> * <li>accessToken: 3scale access token</li> * <li>apiEndpoint: 3scale API endpoint</li> * <li>environment: which environment (e.g. production, staging)</li> * <li>policyConfigUri: apiman policy config to load as JSON from file * ({@link FileResourceFetcher}) or HTTP/S ({@link HttpResourceFetcher}). * See the corresponding fetcher for additional options.</li> * </ul> * * @author Marc Savy {@literal <marc@rhymewithgravy.com>} * @see FileResourceFetcher * @see HttpResourceFetcher * @see AccessTokenResourceFetcher */ @SuppressWarnings("nls") public class ThreeScaleURILoadingRegistry extends InMemoryRegistry implements AsyncInitialize { private static volatile OneShotURILoader instance; private Vertx vertx; private Map<String, String> options; public static final String DEFAULT_NS = "DEFAULT"; /** * @param vertx the vertx instance * @param vxConfig the engine config * @param options the options */ public ThreeScaleURILoadingRegistry(Vertx vertx, IEngineConfig vxConfig, Map<String, String> options) { super(); this.vertx = vertx; this.options = options; } public ThreeScaleURILoadingRegistry(Map<String, String> options) { this(Vertx.vertx(), null, options); } @Override public void initialize(IAsyncResultHandler<Void> resultHandler) { getURILoader(vertx, options).subscribe(this, resultHandler::handle); } private OneShotURILoader getURILoader(Vertx vertx, Map<String, String> options) { if (instance == null) { synchronized(ThreeScaleURILoadingRegistry.class) { if (instance == null) { instance = new OneShotURILoader(vertx, options); } } } return instance; } public static void reloadData(IAsyncHandler<Void> doneHandler) { instance.reload(doneHandler); } public static void reset() { synchronized(ThreeScaleURILoadingRegistry.class) { instance = null; } } @Override public void publishApi(Api api, IAsyncResultHandler<Void> handler) { throw new UnsupportedOperationException(); } @Override public void retireApi(Api api, IAsyncResultHandler<Void> handler) { throw new UnsupportedOperationException(); } @Override public void registerClient(Client client, IAsyncResultHandler<Void> handler) { throw new UnsupportedOperationException(); } @Override public void unregisterClient(Client client, IAsyncResultHandler<Void> handler) { throw new UnsupportedOperationException(); } protected void publishApiInternal(Api api, IAsyncResultHandler<Void> handler) { super.publishApi(api, handler); } protected void registerClientInternal(Client client, IAsyncResultHandler<Void> handler) { super.registerClient(client, handler); } private static final class OneShotURILoader { private Vertx vertx; private URI apiUri; private URI policyConfigUri; private Map<String, String> config; private List<IAsyncResultHandler<Void>> failureHandlers = new ArrayList<>(); private Deque<ThreeScaleURILoadingRegistry> awaiting = new ArrayDeque<>(); private List<ThreeScaleURILoadingRegistry> allRegistries = new ArrayList<>(); private boolean dataProcessed = false; private List<ProxyConfigRoot> configs = new ArrayList<>(); private List<Client> clients = new ArrayList<>(); private List<Api> apis = new ArrayList<>(); private Logger log = LoggerFactory.getLogger(OneShotURILoader.class); private IAsyncHandler<Void> reloadHandler; private List<Api> policyConfigApis = Collections.emptyList(); private String environment; public OneShotURILoader(Vertx vertx, Map<String, String> config) { this.config = config; this.vertx = vertx; apiUri = URI.create(requireOpt("apiEndpoint", "apiEndpoint is required in configuration")); environment = config.getOrDefault("environment", "production"); if (config.containsKey("policyConfigUri")) policyConfigUri = URI.create(config.get("policyConfigUri")); // Can be null. fetchResource(); } private String requireOpt(String key, String errorMsg) { Arguments.require(config.containsKey(key), errorMsg); return config.get(key); } // Clear all registries and add back to processing queue. public synchronized void reload(IAsyncHandler<Void> reloadHandler) { this.reloadHandler = reloadHandler; awaiting.addAll(allRegistries); apis.clear(); clients.clear(); failureHandlers.clear(); allRegistries.stream() .map(ThreeScaleURILoadingRegistry::getMap) .forEach(Map::clear); dataProcessed = false; // Load again from scratch. fetchResource(); } private void fetchResource() { log.debug("Fetching 3scale services..."); // Fetch list of all services getServicesRoot(servicesRoot -> { List<Service> services = servicesRoot.getServices(); // Get all service IDs List<Long> sids = services.stream() .map(service -> service.getService().getId()) .collect(Collectors.toList()); // Get all configs for given service IDs. @SuppressWarnings("rawtypes") List<Future> configFutures = sids.stream() .map(this::getConfig) .collect(Collectors.toList()); // If policyConfigUri is provided, then load API policy config. // NB: THESE ARE API BEANSs WITH *ONLY* POLICY CONFIG! if (policyConfigUri != null) { configFutures.add(fetchPolicyConfig()); } CompositeFuture.all(configFutures) .setHandler(result -> { if (result.succeeded()) { processData(); } else { failAll(result.cause()); } }); }); } private Future<List<Api>> fetchPolicyConfig() { log.debug("Loading policy configuration from {0}...", policyConfigUri); Future<List<Api>> apiResultFuture = Future.future(); new PolicyConfigLoader(vertx, policyConfigUri, config) .setApiResultHandler(apis -> { this.policyConfigApis = apis; apiResultFuture.complete(); }) .setExceptionHandler(apiResultFuture::fail) .load(); return apiResultFuture; } @SuppressWarnings("rawtypes") private Future getConfig(long id) { Future future = Future.future(); String path = String.format("/admin/api/services/%d/proxy/configs/%s/latest.json", id, environment); new AccessTokenResourceFetcher(vertx, config, joinPath(path)) .exceptionHandler(future::fail) .fetch(buffer -> { if (buffer.length() > 0) { ProxyConfigRoot pc = Json.decodeValue(buffer.toString(), ProxyConfigRoot.class); log.debug("Received Proxy Config: {0}", pc); configs.add(pc); } future.complete(); }); return future; } private void getServicesRoot(Handler<ServicesRoot> resultHandler) { new AccessTokenResourceFetcher(vertx, config, joinPath("/admin/api/services.json")) .exceptionHandler(this::failAll) .fetch(buffer -> { ServicesRoot sr = Json.decodeValue(buffer.toString(), ServicesRoot.class); System.out.println("Received buffer"); //log.debug("Received Services: {0}", sr); resultHandler.handle(sr); }); } // Bit messy, refactor private URI joinPath(String path) { try { return new URL(apiUri.toURL(), path).toURI(); } catch (MalformedURLException | URISyntaxException e) { throw new RuntimeException(e); } } private void processData() { if (configs.size() == 0) { log.warn("File loaded into registry was empty. No entities created."); return; } try { // Naive version initially. for (ProxyConfigRoot root : configs) { // Reflects the remote data structure. Content config = root.getProxyConfig().getContent(); Api api = new Api(); api.setApiId(config.getSystemName()); api.setOrganizationId(DEFAULT_NS); api.setEndpoint(config.getProxy().getApiBackend()); api.setEndpointContentType("text/json"); // don't think there is an equivalent of this in 3scale api.setEndpointType("rest"); //don't think there is an equivalent of this in 3scale api.setParsePayload(false); // can let user override this? api.setPublicAPI(true); // is there an equivalent of this? api.setVersion(DEFAULT_NS); // don't think this is relevant anymore setPolicies(api, root); log.info("Processing - {0}: ", config); log.info("Creating API - {0}: ", api); apis.add(api); } dataProcessed = true; checkQueue(); } catch (DecodeException e) { failAll(e); } } private void setPolicies(Api api, ProxyConfigRoot config) { // FIXME optimise // Add 3scale policy Policy pol = new Policy(); pol.setPolicyImpl(determinePolicyImpl()); // TODO get version? Hmm! Env? pol.setPolicyJsonConfig(Json.encode(config)); api.getApiPolicies().add(pol); // Add any policies user specified in remote config. policyConfigApis.stream() .filter(skeleton -> skeleton.getApiId().equals(api.getApiId())) // Apply policies from skeleton to 3scale API. .forEach(skeleton -> api.getApiPolicies().addAll(skeleton.getApiPolicies())); } private String determinePolicyImpl() { String version = config.getOrDefault("pluginVersion", Version.get().getVersionString()); return "plugin:io.apiman.plugins:apiman-plugins-3scale-auth:" + version + ":war/io.apiman.plugins.auth3scale.Auth3Scale"; } public synchronized void subscribe(ThreeScaleURILoadingRegistry registry, IAsyncResultHandler<Void> failureHandler) { Objects.requireNonNull(registry, "registry must be non-null."); Objects.requireNonNull(failureHandler, "failure handler must be non-null."); failureHandlers.add(failureHandler); allRegistries.add(registry); awaiting.add(registry); vertx.runOnContext(action -> checkQueue()); } private void checkQueue() { if (dataProcessed && awaiting.size() > 0) { loadDataIntoRegistries(); } } private void loadDataIntoRegistries() { ThreeScaleURILoadingRegistry reg = null; while ((reg = awaiting.poll()) != null) { log.debug("Loading data into registry {0}: ", reg); for (Api api : apis) { reg.publishApiInternal(api, handleAnyFailure()); log.debug("Publishing: {0} ", api); } for (Client client : clients) { reg.registerClientInternal(client, handleAnyFailure()); log.debug("Registering: {0} ", client); } } if (reloadHandler != null) reloadHandler.handle((Void) null); } private IAsyncResultHandler<Void> handleAnyFailure() { return result -> { if (result.isError()) { failAll(result.getError()); throw new RuntimeException(result.getError()); } }; } private void failAll(Throwable cause) { AsyncResultImpl<Void> failure = AsyncResultImpl.create(cause); failureHandlers.stream().forEach(failureHandler -> { vertx.runOnContext(run -> failureHandler.handle(failure)); }); } } }