package com.hubspot.baragon.agent.lbs; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.io.Files; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.hubspot.baragon.agent.BaragonAgentServiceModule; import com.hubspot.baragon.agent.config.BaragonAgentConfiguration; import com.hubspot.baragon.exceptions.InvalidConfigException; import com.hubspot.baragon.exceptions.LbAdapterExecuteException; import com.hubspot.baragon.exceptions.LockTimeoutException; import com.hubspot.baragon.exceptions.MissingTemplateException; import com.hubspot.baragon.models.BaragonConfigFile; import com.hubspot.baragon.models.BaragonService; import com.hubspot.baragon.models.ServiceContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public class FilesystemConfigHelper { public static final String BACKUP_FILENAME_SUFFIX = ".old"; public static final String FAILED_CONFIG_SUFFIX = ".failed"; private static final Logger LOG = LoggerFactory.getLogger(FilesystemConfigHelper.class); private final LbConfigGenerator configGenerator; private final LocalLbAdapter adapter; private final ReentrantLock agentLock; private final long agentLockTimeoutMs; private final BaragonAgentConfiguration configuration; @Inject public FilesystemConfigHelper(LbConfigGenerator configGenerator, LocalLbAdapter adapter, BaragonAgentConfiguration configuration, @Named(BaragonAgentServiceModule.AGENT_LOCK) ReentrantLock agentLock, @Named(BaragonAgentServiceModule.AGENT_LOCK_TIMEOUT_MS) long agentLockTimeoutMs) { this.configGenerator = configGenerator; this.adapter = adapter; this.configuration = configuration; this.agentLock = agentLock; this.agentLockTimeoutMs = agentLockTimeoutMs; } public void remove(BaragonService service) throws LbAdapterExecuteException, IOException { for (String filename : configGenerator.getConfigPathsForProject(service)) { File file = new File(filename); if (!file.exists()) { continue; } if (!file.delete()) { throw new RuntimeException(String.format("Failed to remove %s for %s", filename, service.getServiceId())); } } } public void checkAndReload() throws InvalidConfigException, LbAdapterExecuteException, IOException, InterruptedException, LockTimeoutException { if (!agentLock.tryLock(agentLockTimeoutMs, TimeUnit.MILLISECONDS)) { throw new LockTimeoutException(String.format("Timed out waiting to acquire lock for reload"), agentLock); } try { adapter.checkConfigs(); adapter.reloadConfigs(); } catch (Exception e) { LOG.error("Caught exception while trying to reload configs", e); throw Throwables.propagate(e); } finally { agentLock.unlock(); } } public Optional<Collection<BaragonConfigFile>> configsToApply(ServiceContext context) throws MissingTemplateException { final BaragonService service = context.getService(); final boolean previousConfigsExist = configsExist(service); Collection<BaragonConfigFile> newConfigs = configGenerator.generateConfigsForProject(context); if (previousConfigsExist && configsMatch(newConfigs, readConfigs(service))) { return Optional.absent(); } else { return Optional.of(newConfigs); } } public boolean configsMatch(Collection<BaragonConfigFile> newConfigs, Collection<BaragonConfigFile> currentConfigs) { return currentConfigs.containsAll(newConfigs); } public void bootstrapApply(ServiceContext context, Collection<BaragonConfigFile> newConfigs) throws InvalidConfigException, LbAdapterExecuteException, IOException, MissingTemplateException { final BaragonService service = context.getService(); final boolean previousConfigsExist = configsExist(service); LOG.info(String.format("Going to apply %s: %s", service.getServiceId(), Joiner.on(", ").join(context.getUpstreams()))); backupConfigs(service); try { writeConfigs(newConfigs); adapter.checkConfigs(); } catch (Exception e) { LOG.error(String.format("Caught exception while writing configs for %s, reverting to backups!", service.getServiceId()), e); saveAsFailed(service); if (previousConfigsExist) { restoreConfigs(service); } else { remove(service); } throw Throwables.propagate(e); } LOG.info(String.format("Apply finished for %s", service.getServiceId())); } public void apply(ServiceContext context, Optional<BaragonService> maybeOldService, boolean revertOnFailure, boolean noReload, boolean noValidate, boolean delayReload, Optional<Integer> batchItemNumber) throws InvalidConfigException, LbAdapterExecuteException, IOException, MissingTemplateException, InterruptedException, LockTimeoutException { final BaragonService service = context.getService(); final BaragonService oldService = maybeOldService.or(service); LOG.info(String.format("Going to apply %s: %s", service.getServiceId(), Joiner.on(", ").join(context.getUpstreams()))); final boolean oldServiceExists = configsExist(oldService); final boolean previousConfigsExist = configsExist(service); Collection<BaragonConfigFile> newConfigs = configGenerator.generateConfigsForProject(context); if (!agentLock.tryLock(agentLockTimeoutMs, TimeUnit.MILLISECONDS)) { throw new LockTimeoutException("Timed out waiting to acquire lock", agentLock); } try { if (configsMatch(newConfigs, readConfigs(oldService))) { LOG.info("Configs are unchanged, skipping apply"); if (!noReload && !delayReload && batchItemNumber.isPresent() && batchItemNumber.get() > 1) { LOG.debug("Item is the last in a batch, reloading configs"); adapter.reloadConfigs(); } return; } // Backup configs if (revertOnFailure) { backupConfigs(service); if (oldServiceExists) { backupConfigs(oldService); } } // Write & check the configs if (context.isPresent()) { writeConfigs(newConfigs); //If the new service id for this base path is different, remove the configs for the old service id if (oldServiceExists && !oldService.getServiceId().equals(service.getServiceId())) { remove(oldService); } } else { remove(service); } if (!noValidate) { adapter.checkConfigs(); } else { LOG.debug("Not validating configs due to 'noValidate' specified in request"); } if (!noReload && !delayReload) { adapter.reloadConfigs(); } else { LOG.debug("Not reloading configs: {}", noReload ? "'noReload' specified in request" : "Will reload at end of request batch"); } } catch (Exception e) { LOG.error(String.format("Caught exception while writing configs for %s, reverting to backups!", service.getServiceId()), e); saveAsFailed(service); // Restore configs if (revertOnFailure) { if (oldServiceExists && !oldService.equals(service)) { restoreConfigs(oldService); } if (previousConfigsExist) { restoreConfigs(service); } else { remove(service); } } throw Throwables.propagate(e); } finally { agentLock.unlock(); } removeBackupConfigs(oldService); LOG.info(String.format("Apply finished for %s", service.getServiceId())); } public void delete(BaragonService service, Optional<BaragonService> maybeOldService, boolean noReload, boolean noValidate, boolean delayReload) throws InvalidConfigException, LbAdapterExecuteException, IOException, MissingTemplateException, InterruptedException, LockTimeoutException { final boolean oldServiceExists = (maybeOldService.isPresent() && configsExist(maybeOldService.get())); final boolean previousConfigsExist = configsExist(service); if (!agentLock.tryLock(agentLockTimeoutMs, TimeUnit.MILLISECONDS)) { throw new LockTimeoutException("Timed out waiting to acquire lock for delete", agentLock); } try { if (previousConfigsExist) { backupConfigs(service); remove(service); } if (oldServiceExists && !maybeOldService.get().equals(service)) { backupConfigs(maybeOldService.get()); remove(maybeOldService.get()); } if (!noValidate) { adapter.checkConfigs(); } else { LOG.debug("Not validating configs due to 'noValidate' specified in request"); } if (!noReload && !delayReload) { adapter.reloadConfigs(); } else { LOG.debug("Not reloading configs: {}", noReload ? "'noReload' specified in request" : "Will reload at end of request batch"); } } catch (Exception e) { LOG.error(String.format("Caught exception while deleting configs for %s, reverting to backups!", service.getServiceId()), e); saveAsFailed(service); if (oldServiceExists && !maybeOldService.get().equals(service)) { restoreConfigs(maybeOldService.get()); } if (previousConfigsExist) { restoreConfigs(service); } else { remove(service); } throw Throwables.propagate(e); } finally { agentLock.unlock(); } } private void writeConfigs(Collection<BaragonConfigFile> files) { for (BaragonConfigFile file : files) { try { File configFile = new File(file.getFullPath()); if (configFile.getParentFile() != null) { if (!configFile.getParentFile().exists() && !configFile.getParentFile().mkdirs()) { throw new IOException(String.format("Could not create parent directories for file path %s", file.getFullPath())); } } Files.write(file.getContent().getBytes(Charsets.UTF_8), new File(file.getFullPath())); } catch (IOException e) { LOG.error(String.format("Failed writing %s", file.getFullPath()), e); throw new RuntimeException(String.format("Failed writing %s", file.getFullPath()), e); } } } private Collection<BaragonConfigFile> readConfigs(BaragonService service) { final Collection<BaragonConfigFile> configs = new ArrayList<>(); for (String filename : configGenerator.getConfigPathsForProject(service)) { File file = new File(filename); if (!file.exists()) { continue; } try { configs.add(new BaragonConfigFile(filename, Files.asCharSource(file, Charsets.UTF_8).read())); } catch (IOException e) { throw Throwables.propagate(e); } } return configs; } private void backupConfigs(BaragonService service) { for (String filename : configGenerator.getConfigPathsForProject(service)) { try { File src = new File(filename); if (!src.exists()) { continue; } File dest = new File(filename + BACKUP_FILENAME_SUFFIX); Files.move(src, dest); } catch (IOException e) { LOG.error(String.format("Failed to backup %s", filename), e); throw new RuntimeException(String.format("Failed to backup %s", filename)); } } } private void removeBackupConfigs(BaragonService service) { for (String filename : configGenerator.getConfigPathsForProject(service)) { File file = new File(filename + BACKUP_FILENAME_SUFFIX); if (!file.exists()) { continue; } } } private boolean configsExist(BaragonService service) { for (String filename : configGenerator.getConfigPathsForProject(service)) { if (new File(filename).exists()) { return true; } } return false; } private void saveAsFailed(BaragonService service) { if (configuration.isSaveFailedConfigs()) { for (String filename : configGenerator.getConfigPathsForProject(service)) { try { File src = new File(filename); if (!src.exists()) { continue; } File dest = new File(filename + FAILED_CONFIG_SUFFIX); Files.copy(src, dest); } catch (IOException e) { LOG.warn(String.format("Failed to save failed config %s", filename), e); } } } } private void restoreConfigs(BaragonService service) { for (String filename : configGenerator.getConfigPathsForProject(service)) { try { File src = new File(filename + BACKUP_FILENAME_SUFFIX); if (!src.exists()) { continue; } File dest = new File(filename); Files.copy(src, dest); } catch (IOException e) { LOG.error(String.format("Failed to restore %s", filename), e); throw new RuntimeException(String.format("Failed to restore %s", filename)); } } } }