package com.netflix.appinfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Provider; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * A holder class for AmazonInfo that exposes some APIs to allow for refreshes. */ public class RefreshableAmazonInfoProvider implements Provider<AmazonInfo> { /** * A fallback provider for a default set of IP and hostname if equivalent data are not available * from the EC2 metadata url. */ public static interface FallbackAddressProvider { String getFallbackIp(); String getFallbackHostname(); } private static final Logger logger = LoggerFactory.getLogger(RefreshableAmazonInfoProvider.class); /* Visible for testing */ volatile AmazonInfo info; private final AmazonInfoConfig amazonInfoConfig; public RefreshableAmazonInfoProvider(AmazonInfoConfig amazonInfoConfig, FallbackAddressProvider fallbackAddressProvider) { this(init(amazonInfoConfig, fallbackAddressProvider), amazonInfoConfig); } /* visible for testing */ RefreshableAmazonInfoProvider(AmazonInfo initialInfo, AmazonInfoConfig amazonInfoConfig) { this.amazonInfoConfig = amazonInfoConfig; this.info = initialInfo; } private static AmazonInfo init(AmazonInfoConfig amazonInfoConfig, FallbackAddressProvider fallbackAddressProvider) { AmazonInfo info; try { info = AmazonInfo.Builder .newBuilder() .withAmazonInfoConfig(amazonInfoConfig) .autoBuild(amazonInfoConfig.getNamespace()); logger.info("Datacenter is: " + DataCenterInfo.Name.Amazon); } catch (Throwable e) { logger.error("Cannot initialize amazon info :", e); throw new RuntimeException(e); } // Instance id being null means we could not get the amazon metadata if (info.get(AmazonInfo.MetaDataKey.instanceId) == null) { if (amazonInfoConfig.shouldValidateInstanceId()) { throw new RuntimeException( "Your datacenter is defined as cloud but we are not able to get the amazon metadata to " + "register. \nSet the property " + amazonInfoConfig.getNamespace() + "validateInstanceId to false to ignore the metadata call"); } else { // The property to not validate instance ids may be set for // development and in that scenario, populate instance id // and public hostname with the hostname of the machine Map<String, String> metadataMap = new HashMap<String, String>(); metadataMap.put(AmazonInfo.MetaDataKey.instanceId.getName(), fallbackAddressProvider.getFallbackIp()); metadataMap.put(AmazonInfo.MetaDataKey.publicHostname.getName(), fallbackAddressProvider.getFallbackHostname()); info.setMetadata(metadataMap); } } else if ((info.get(AmazonInfo.MetaDataKey.publicHostname) == null) && (info.get(AmazonInfo.MetaDataKey.localIpv4) != null)) { // :( legacy code and logic // This might be a case of VPC where the instance id is not null, but // public hostname might be null info.getMetadata().put(AmazonInfo.MetaDataKey.publicHostname.getName(), (info.get(AmazonInfo.MetaDataKey.localIpv4))); } return info; } /** * Refresh the locally held version of {@link com.netflix.appinfo.AmazonInfo} */ public synchronized void refresh() { try { AmazonInfo newInfo = AmazonInfo.Builder .newBuilder() .withAmazonInfoConfig(amazonInfoConfig) .autoBuild(amazonInfoConfig.getNamespace()); if (shouldUpdate(newInfo, info)) { // the datacenter info has changed, re-sync it logger.info("The AmazonInfo changed from : {} => {}", info, newInfo); this.info = newInfo; } } catch (Throwable t) { logger.error("Cannot refresh the Amazon Info ", t); } } /** * @return the locally held version of {@link com.netflix.appinfo.AmazonInfo} */ @Override public AmazonInfo get() { return info; } /** * Rules of updating AmazonInfo: * - instanceId must exist * - localIp/privateIp must exist * - publicHostname does not necessarily need to exist (e.g. in vpc) */ /* visible for testing */ static boolean shouldUpdate(AmazonInfo newInfo, AmazonInfo oldInfo) { if (newInfo.getMetadata().isEmpty()) { logger.warn("Newly resolved AmazonInfo is empty, skipping an update cycle"); } else if (!newInfo.equals(oldInfo)) { if (isBlank(newInfo.get(AmazonInfo.MetaDataKey.instanceId))) { logger.warn("instanceId is blank, skipping an update cycle"); return false; } else if (isBlank(newInfo.get(AmazonInfo.MetaDataKey.localIpv4))) { logger.warn("localIpv4 is blank, skipping an update cycle"); return false; } else { Set<String> newKeys = new HashSet<>(newInfo.getMetadata().keySet()); Set<String> oldKeys = new HashSet<>(oldInfo.getMetadata().keySet()); Set<String> union = new HashSet<>(newKeys); union.retainAll(oldKeys); newKeys.removeAll(union); oldKeys.removeAll(union); for (String key : newKeys) { logger.info("Adding new metadata {}={}", key, newInfo.getMetadata().get(key)); } for (String key : oldKeys) { logger.info("Removing old metadata {}={}", key, oldInfo.getMetadata().get(key)); } } return true; } return false; } private static boolean isBlank(String str) { return str == null || str.isEmpty(); } }