/*
* Copyright 2013-2014 the original author or authors.
*
* 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 org.springframework.cloud.aws.support;
import com.amazonaws.SDKGlobalConfiguration;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
import com.amazonaws.services.cloudformation.model.DescribeStacksResult;
import com.amazonaws.services.cloudformation.model.Output;
import com.amazonaws.services.cloudformation.model.Stack;
import com.amazonaws.util.EC2MetadataUtils;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Retrieves the instance id output value from the configured stack and exposes it via a local metadata service. In
* addition, sets the AWS SDK internal system property for overwriting the service endpoint url used by the {@link
* com.amazonaws.util.EC2MetadataUtils} to the url of the local metadata service.
*
* @author Christian Stettler
*/
public class TestStackInstanceIdService {
private static final String INSTANCE_ID_SERVICE_HOSTNAME = "localhost";
private static final int INSTANCE_ID_SERVICE_PORT = 12345;
private final InstanceIdSource instanceIdSource;
private HttpServer httpServer;
private TestStackInstanceIdService(InstanceIdSource instanceIdSource) {
this.instanceIdSource = instanceIdSource;
}
public void enable() {
startMetadataHttpServer(this.instanceIdSource.getInstanceId());
overwriteMetadataEndpointUrl("http://" + INSTANCE_ID_SERVICE_HOSTNAME + ":" + INSTANCE_ID_SERVICE_PORT);
}
public void disable() {
resetMetadataEndpointUrlOverwrite();
clearMetadataCache();
stopMetadataHttpServer();
}
private void startMetadataHttpServer(String instanceId) {
try {
this.httpServer = HttpServer.create(new InetSocketAddress(INSTANCE_ID_SERVICE_HOSTNAME, INSTANCE_ID_SERVICE_PORT), -1);
this.httpServer.createContext("/latest/meta-data/instance-id", new InstanceIdHttpHandler(instanceId));
this.httpServer.start();
} catch (IOException e) {
throw new IllegalStateException("Unable to start metadata http server", e);
}
}
private void stopMetadataHttpServer() {
if (this.httpServer != null) {
this.httpServer.stop(0);
}
}
public static TestStackInstanceIdService fromStackOutputKey(String stackName, String outputKey, AmazonCloudFormation amazonCloudFormationClient) {
return new TestStackInstanceIdService(new AmazonStackOutputBasedInstanceIdSource(stackName, outputKey, amazonCloudFormationClient));
}
public static TestStackInstanceIdService fromInstanceId(String instanceId) {
return new TestStackInstanceIdService(new StaticInstanceIdSource(instanceId));
}
private static void overwriteMetadataEndpointUrl(String localMetadataServiceEndpointUrl) {
System.setProperty(SDKGlobalConfiguration.EC2_METADATA_SERVICE_OVERRIDE_SYSTEM_PROPERTY, localMetadataServiceEndpointUrl);
}
private static void resetMetadataEndpointUrlOverwrite() {
System.clearProperty(SDKGlobalConfiguration.EC2_METADATA_SERVICE_OVERRIDE_SYSTEM_PROPERTY);
}
private static void clearMetadataCache() {
try {
Field metadataCacheField = EC2MetadataUtils.class.getDeclaredField("cache");
metadataCacheField.setAccessible(true);
metadataCacheField.set(null, new HashMap<String, String>());
} catch (Exception e) {
throw new IllegalStateException("Unable to clear metadata cache in '" + EC2MetadataUtils.class.getName() + "'", e);
}
}
/**
* Utility interface for abstracting source for instance id.
*/
private interface InstanceIdSource {
String getInstanceId();
}
/**
* Handler for responding with specified instance id
*/
private static class InstanceIdHttpHandler implements HttpHandler {
private final String instanceId;
private InstanceIdHttpHandler(String instanceId) {
this.instanceId = instanceId;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
httpExchange.sendResponseHeaders(200, this.instanceId.getBytes().length);
OutputStream responseBody = httpExchange.getResponseBody();
responseBody.write(this.instanceId.getBytes());
responseBody.flush();
responseBody.close();
}
}
/**
* Source for statically configured instance id.
* <p>
* Useful for unit testing.
* </p>
*/
private static class StaticInstanceIdSource implements InstanceIdSource {
private final String instanceId;
private StaticInstanceIdSource(String instanceId) {
this.instanceId = instanceId;
}
@Override
public String getInstanceId() {
return this.instanceId;
}
}
/**
* Source for retrieving instance id from specified output key of specified stack. Requires specified stack to be
* available.
* <p>
* Useful for integration testing.
* </p>
*/
private static class AmazonStackOutputBasedInstanceIdSource implements InstanceIdSource {
private final String stackName;
private final String outputKey;
private final AmazonCloudFormation amazonCloudFormationClient;
private AmazonStackOutputBasedInstanceIdSource(String stackName, String outputKey, AmazonCloudFormation amazonCloudFormationClient) {
this.stackName = stackName;
this.outputKey = outputKey;
this.amazonCloudFormationClient = amazonCloudFormationClient;
}
private static Stack getStack(DescribeStacksResult describeStacksResult, String stackName) {
for (Stack stack : describeStacksResult.getStacks()) {
if (stack.getStackName().equals(stackName)) {
return stack;
}
}
throw new IllegalStateException("No stack found with name '" + stackName + "' (available stacks: " + allStackNames(describeStacksResult) + ")");
}
@Override
public String getInstanceId() {
DescribeStacksResult describeStacksResult = this.amazonCloudFormationClient.describeStacks(new DescribeStacksRequest());
Stack stack = getStack(describeStacksResult, this.stackName);
return getOutputValue(stack, this.outputKey);
}
private static String getOutputValue(Stack stack, String outputKey) {
for (Output output : stack.getOutputs()) {
if (output.getOutputKey().equals(outputKey)) {
return output.getOutputValue();
}
}
throw new IllegalStateException("No output '" + outputKey + "' defined in stack '" + stack.getStackName() + "'");
}
private static List<String> allStackNames(DescribeStacksResult describeStacksResult) {
List<String> allStackNames = new ArrayList<>();
for (Stack stack : describeStacksResult.getStacks()) {
allStackNames.add(stack.getStackName());
}
return allStackNames;
}
}
}