| /* |
| * 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.slider.agent.rest |
| |
| import com.google.common.base.Preconditions |
| import com.sun.jersey.api.client.Client |
| import com.sun.jersey.api.client.ClientResponse |
| import com.sun.jersey.api.client.UniformInterfaceException |
| import com.sun.jersey.api.client.WebResource |
| import groovy.transform.CompileStatic |
| import groovy.util.logging.Slf4j |
| import org.apache.hadoop.yarn.webapp.NotFoundException |
| import org.apache.slider.api.StateValues |
| import org.apache.slider.api.types.ComponentInformation |
| import org.apache.slider.api.types.ContainerInformation |
| import org.apache.slider.common.tools.SliderUtils |
| import org.apache.slider.core.conf.AggregateConf |
| import org.apache.slider.core.conf.ConfTree |
| import org.apache.slider.core.conf.ConfTreeOperations |
| import org.apache.slider.core.restclient.HttpVerb |
| import org.apache.slider.server.appmaster.web.rest.application.ApplicationResource |
| import org.apache.slider.api.types.PingInformation |
| import org.apache.slider.test.Outcome |
| |
| import javax.net.ssl.SSLException |
| import javax.ws.rs.core.MediaType |
| |
| import static org.apache.slider.api.ResourceKeys.COMPONENT_INSTANCES |
| import static org.apache.slider.api.StatusKeys.* |
| import static org.apache.slider.common.SliderKeys.COMPONENT_AM |
| import static org.apache.slider.server.appmaster.web.rest.RestPaths.* |
| |
| /** |
| * This class contains parts of tests that can be run |
| * against a deployed AM: local or remote. |
| * It uses Jersey WebResource... and must be passed a client |
| * that is either secure or not |
| * {@link WebResource} |
| * |
| * |
| */ |
| @CompileStatic |
| @Slf4j |
| class JerseyTestDelegates extends AbstractRestTestDelegate { |
| |
| final String appmaster; |
| final String application; |
| final Client jersey; |
| final WebResource amResource |
| final WebResource appResource |
| |
| |
| JerseyTestDelegates(String appmaster, Client jersey, |
| boolean enableComplexVerbs = true) { |
| super(enableComplexVerbs) |
| this.jersey = jersey |
| this.appmaster = appmaster |
| application = appendToURL(appmaster, SLIDER_PATH_APPLICATION) |
| amResource = jersey.resource(appmaster) |
| amResource.type(MediaType.APPLICATION_JSON) |
| appResource = amResource.path(SLIDER_PATH_APPLICATION) |
| } |
| |
| /** |
| * <T> T get(Class<T> c) |
| * Get operation against a path under the Application |
| * @param subpath path |
| * @return |
| */ |
| public <T> T jGetApplicationResource(String subpath, Class<T> c) { |
| return (T)jExec(HttpVerb.GET, subpath, c); |
| } |
| |
| /** |
| * <T> T get(Class<T> c) |
| * Get operation against a path under the Application |
| * @param subpath path |
| * @return |
| */ |
| public <T> T jExec(HttpVerb method, String subpath, Class<T> c) { |
| WebResource resource = applicationResource(subpath); |
| jExec(method, resource, c); |
| } |
| |
| public <T> T jExec(HttpVerb method, WebResource resource, Class<T> c) { |
| try { |
| Preconditions.checkArgument(c != null); |
| resource.accept(MediaType.APPLICATION_JSON_TYPE); |
| (T) resource.method(method.verb, c); |
| } catch (UniformInterfaceException ex) { |
| uprateFaults(method, resource, ex); |
| // never reached as the uprate throws the new exception |
| return null; |
| } |
| } |
| |
| /** |
| * Create a resource under the application path |
| * @param subpath |
| * @return |
| */ |
| public WebResource applicationResource(String subpath) { |
| return appResource.path(subpath) |
| } |
| |
| /** |
| * Convert faults to exceptions; pass through 200 responses |
| * @param method |
| * @param webResource |
| * @param ex |
| * @return |
| */ |
| public void uprateFaults( |
| HttpVerb method, |
| WebResource webResource, |
| UniformInterfaceException ex) { |
| uprateFaults(method.verb, |
| webResource.URI.toString(), |
| ex.response.status, |
| ex.response.toString()) |
| } |
| |
| /** |
| * <T> T get(Class<T> c) |
| * Get operation against a path under the AM |
| * @param path path |
| * @return |
| */ |
| public <T> T jGetAMResource(String path, Class<T> c) { |
| assert c |
| WebResource webResource = buildResource(path) |
| (T)webResource.get(c) |
| } |
| |
| /** |
| * Get operation against a path under the AM |
| * @param path path |
| * @return the string value |
| */ |
| public String jerseyGet(String path) { |
| return jGetAMResource(path, String.class) |
| } |
| |
| /** |
| * Build a resource against a path under the AM API |
| * @param path path |
| * @return a resource for use |
| */ |
| public WebResource buildResource(String path) { |
| assert path |
| String fullpath = SliderUtils.appendToURL(appmaster, path); |
| WebResource webResource = jersey.resource(fullpath); |
| webResource.type(MediaType.APPLICATION_JSON); |
| log.info("HTTP operation against $fullpath"); |
| return webResource; |
| } |
| |
| public void testJerseyGetConftree() throws Throwable { |
| jGetApplicationResource(LIVE_RESOURCES, ConfTree.class); |
| } |
| |
| public void testCodahaleOperations() throws Throwable { |
| describe "Codahale operations" |
| jerseyGet("/") |
| jerseyGet(SYSTEM_THREADS) |
| jerseyGet(SYSTEM_HEALTHCHECK) |
| jerseyGet(SYSTEM_PING) |
| jerseyGet(SYSTEM_METRICS_JSON) |
| } |
| |
| public void logCodahaleMetrics() { |
| // query Coda Hale metrics |
| log.info jerseyGet(SYSTEM_HEALTHCHECK) |
| log.info jerseyGet(SYSTEM_METRICS) |
| } |
| |
| /** |
| * Fetch a typed entry <i>under the application path</i> |
| * @param subpath |
| * @param clazz |
| * @return |
| */ |
| public <T> T jFetchType( |
| String subpath, Class<T> clazz) { |
| (T)jGetApplicationResource(subpath, clazz) |
| } |
| |
| public ConfTreeOperations jGetConfigTree( |
| String path) { |
| ConfTree ctree = jGetApplicationResource(path, ConfTree) |
| ConfTreeOperations tree = new ConfTreeOperations(ctree) |
| return tree |
| } |
| |
| |
| public void testMimeTypes() throws Throwable { |
| describe "Mime Types" |
| |
| WebResource resource = applicationResource(LIVE_RESOURCES) |
| def response = resource.get(ClientResponse) |
| response.headers.each {key, val -> log.info("$key: $val")} |
| log.info response.toString() |
| assert response.type.equals(MediaType.APPLICATION_JSON_TYPE) |
| } |
| |
| |
| public void testLiveResources() throws Throwable { |
| describe "Live Resources" |
| |
| ConfTreeOperations tree = jGetConfigTree(LIVE_RESOURCES) |
| |
| log.info tree.toString() |
| def liveAM = tree.getComponent(COMPONENT_AM) |
| def desiredInstances = liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES); |
| assert desiredInstances == |
| liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_ACTUAL) |
| |
| assert 1 == liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_STARTED) |
| assert 0 == liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_REQUESTING) |
| assert 0 == liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_FAILED) |
| assert 0 == liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_COMPLETED) |
| assert 0 == liveAM.getMandatoryOptionInt(COMPONENT_INSTANCES_RELEASING) |
| } |
| |
| public void testLiveContainers() throws Throwable { |
| describe "Application REST ${LIVE_CONTAINERS}" |
| |
| Map<String, ContainerInformation> containers = |
| jGetApplicationResource(LIVE_CONTAINERS, HashMap) |
| assert containers.size() == 1 |
| log.info "${containers}" |
| ContainerInformation amContainerInfo = |
| (ContainerInformation) containers.values()[0] |
| assert amContainerInfo.containerId |
| |
| def amContainerId = amContainerInfo.containerId |
| assert containers[amContainerId] |
| |
| assert amContainerInfo.component == COMPONENT_AM |
| assert amContainerInfo.createTime > 0 |
| assert amContainerInfo.exitCode == null |
| assert amContainerInfo.output == null |
| assert amContainerInfo.released == null |
| assert amContainerInfo.state == StateValues.STATE_LIVE |
| |
| describe "containers" |
| |
| ContainerInformation retrievedContainerInfo = |
| jFetchType( |
| LIVE_CONTAINERS + "/${amContainerId}", |
| ContainerInformation |
| ) |
| assert retrievedContainerInfo.containerId == amContainerId |
| |
| // fetch missing |
| try { |
| def result = jFetchType( |
| LIVE_CONTAINERS + "/unknown", |
| ContainerInformation |
| ) |
| fail("expected an error, got $result") |
| } catch (NotFoundException e) { |
| // expected |
| } |
| |
| |
| describe "components" |
| |
| Map<String, ComponentInformation> components = |
| jFetchType(LIVE_COMPONENTS, HashMap) |
| // two components |
| assert components.size() >= 1 |
| log.info "${components}" |
| ComponentInformation amFullInfo = jFetchType( |
| LIVE_COMPONENTS + "/${COMPONENT_AM}", |
| ComponentInformation) |
| |
| assert amFullInfo.containers.size() == 1 |
| assert amFullInfo.containers[0] == amContainerId |
| |
| } |
| |
| /** |
| * Assert that a path resolves to an array list that contains |
| * those entries (and only those entries) expected |
| * @param appmaster AM ref |
| * @param path path under AM |
| * @param entries entries to assert the presence of |
| */ |
| public void assertPathServesList( |
| String appmaster, |
| String path, |
| List<String> entries) { |
| def list = jFetchType(path, ArrayList) |
| assert list.size() == entries.size() |
| assert entries.containsAll(list) |
| } |
| |
| /** |
| * Fetch a list of URLs, all of which must be of the same type |
| * @param clazz class of resolved values |
| * @param appmaster URL to app master |
| * @param subpaths list of subpaths |
| * @return a map of paths to values |
| */ |
| public <T> Map<String, T> fetchTypeList( |
| Class<T> clazz, String appmaster, List<String> subpaths |
| ) { |
| Map<String, T> results = [:] |
| subpaths.each { String it -> |
| results[it] = (jFetchType(it, clazz)) |
| } |
| return results; |
| } |
| |
| /** |
| * Test the rest model. For this to work the cluster has to be configured |
| * with the global option |
| * @param appmaster |
| */ |
| public void testRESTModel() { |
| describe "model" |
| |
| assertPathServesList(appmaster, |
| MODEL, |
| ApplicationResource.MODEL_ENTRIES) |
| |
| def unresolvedConf = jFetchType(MODEL_DESIRED, AggregateConf) |
| // log.info "Unresolved \n$unresolvedConf" |
| def unresolvedAppConf = unresolvedConf.appConfOperations |
| |
| def sam = "slider-appmaster" |
| assert unresolvedAppConf.getComponentOpt(sam, TEST_GLOBAL_OPTION, "") == "" |
| def resolvedConf = jFetchType(MODEL_RESOLVED, AggregateConf) |
| // log.info "Resolved \n$resolvedConf" |
| assert resolvedConf.appConfOperations.getComponentOpt( |
| sam, TEST_GLOBAL_OPTION, "") == TEST_GLOBAL_OPTION_PRESENT |
| |
| def unresolved = fetchTypeList(ConfTree, appmaster, |
| [MODEL_DESIRED_APPCONF, MODEL_DESIRED_RESOURCES]) |
| assert unresolved[MODEL_DESIRED_APPCONF].components[sam] |
| [TEST_GLOBAL_OPTION] == null |
| |
| |
| def resolved = fetchTypeList(ConfTree, appmaster, |
| [MODEL_RESOLVED_APPCONF, MODEL_RESOLVED_RESOURCES]) |
| assert resolved[MODEL_RESOLVED_APPCONF]. |
| components[sam][TEST_GLOBAL_OPTION] == TEST_GLOBAL_OPTION_PRESENT |
| } |
| |
| public void testPing() { |
| // GET |
| describe "pinging" |
| |
| def pinged = jExec(HttpVerb.GET, ACTION_PING, PingInformation) |
| log.info "Ping GET: $pinged" |
| // HEAD |
| // jExec(HttpVerb.HEAD, ACTION_PING, PingResource) |
| jExec(HttpVerb.PUT, ACTION_PING, PingInformation) |
| jExec(HttpVerb.DELETE, ACTION_PING, PingInformation) |
| jExec(HttpVerb.POST, ACTION_PING, PingInformation) |
| ping(HttpVerb.PUT, ACTION_PING, "ping-text") |
| ping(HttpVerb.POST, ACTION_PING, "ping-text") |
| ping(HttpVerb.DELETE, ACTION_PING, "ping-text") |
| } |
| |
| /** |
| * Execute a ping; assert that a response came back with the relevant |
| * verb if the verb has a response body |
| * @param method method to invoke |
| * @param subpath ping path |
| * @param payload payload |
| * @return the resource if the verb has a response |
| */ |
| private PingInformation ping(HttpVerb method, String subpath, Object payload) { |
| def actionPing = applicationResource(ACTION_PING) |
| def upload = method.hasUploadBody() ? payload : null |
| if (method.hasResponseBody()) { |
| def pinged = actionPing.method(method.verb, PingInformation, upload) |
| assert method.verb == pinged.verb |
| return pinged |
| } else { |
| actionPing.method(method.verb, upload) |
| return null |
| } |
| } |
| |
| /** |
| * Test the stop command. |
| * Important: once executed, the AM is no longer there. |
| * This must be the last test in the sequence. |
| */ |
| /* |
| |
| public void testStop() { |
| String target = appendToURL(appmaster, SLIDER_PATH_APPLICATION, ACTION_STOP) |
| describe "Stop URL $target" |
| URL targetUrl = new URL(target) |
| def outcome = connectionOperations.execHttpOperation( |
| HttpVerb.POST, |
| targetUrl, |
| new byte[0], |
| MediaType.TEXT_PLAIN) |
| log.info "Stopped: $outcome" |
| |
| // await the shutdown |
| sleep(1000) |
| |
| // now a ping is expected to fail |
| String ping = appendToURL(appmaster, SLIDER_PATH_APPLICATION, ACTION_PING) |
| URL pingUrl = new URL(ping) |
| |
| repeatUntilSuccess("probe for missing registry entry", |
| this.&probePingFailing, 30000, 500, |
| [url: ping], |
| true, |
| "AM failed to shut down") { |
| def pinged = jFetchType(ACTION_PING + "?body=hello", |
| PingResource |
| ) |
| fail("AM didn't shut down; Ping GET= $pinged") |
| } |
| |
| } |
| */ |
| |
| /** |
| * Probe that spins until the url specified by "url") refuses |
| * connections |
| * @param args argument map |
| * @return the outcome |
| */ |
| /* |
| Outcome probePingFailing(Map args) { |
| String ping = args["url"] |
| URL pingUrl = new URL(ping) |
| try { |
| def response = pingAction(HttpVerb.HEAD, pingUrl, "should not be running") |
| return Outcome.Retry |
| } catch (IOException e) { |
| // expected |
| return Outcome.Success |
| } |
| } |
| */ |
| |
| public void testSuiteGetOperations() { |
| |
| testCodahaleOperations() |
| testMimeTypes() |
| testJerseyGetConftree() |
| testLiveResources() |
| testLiveContainers(); |
| testRESTModel() |
| } |
| |
| public void testSuiteComplexVerbs() { |
| testPing(); |
| } |
| |
| /** |
| * Probe callback for is the web service live; currently |
| * checks the "/system/health" path. |
| * Raising an SSL exception is considered a sign of liveness. |
| * @param args args: ignored |
| * @return the outcome |
| */ |
| Outcome probeServiceLive(Map<String, String> args) { |
| try { |
| jerseyGet(SYSTEM_HEALTHCHECK) |
| return Outcome.Success |
| } catch (SSLException e) { |
| // SSL exception => success |
| return Outcome.Success |
| } catch (IOException e) { |
| def cause = e.getCause() |
| if (cause && cause instanceof SSLException) { |
| // nested SSL exception => success |
| return Outcome.Success |
| } |
| // any other IOE is a retry |
| return Outcome.Retry |
| } |
| } |
| |
| |
| } |