/*************************GO-LICENSE-START********************************* * Copyright 2014 ThoughtWorks, 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. *************************GO-LICENSE-END***********************************/ package com.thoughtworks.go.config; import com.thoughtworks.go.config.remote.ConfigRepoConfig; import com.thoughtworks.go.config.remote.ConfigReposConfig; import com.thoughtworks.go.config.remote.PartialConfig; import com.thoughtworks.go.config.remote.RepoConfigOrigin; import com.thoughtworks.go.domain.config.Configuration; import com.thoughtworks.go.domain.materials.MaterialConfig; import com.thoughtworks.go.serverhealth.HealthStateScope; import com.thoughtworks.go.serverhealth.HealthStateType; import com.thoughtworks.go.serverhealth.ServerHealthService; import com.thoughtworks.go.serverhealth.ServerHealthState; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Parses partial configurations and exposes latest configurations as soon as possible. */ @Component public class GoRepoConfigDataSource implements ChangedRepoConfigWatchListListener { private static final Logger LOGGER = Logger.getLogger(GoRepoConfigDataSource.class); private GoConfigPluginService configPluginService; private GoConfigWatchList configWatchList; private final ServerHealthService serverHealthService; // value is partial config instance or last exception private Map<String,PartialConfigParseResult> fingerprintOfPartialToLatestParseResultMap = new ConcurrentHashMap<>(); private List<PartialConfigUpdateCompletedListener> listeners = new ArrayList<>(); @Autowired public GoRepoConfigDataSource(GoConfigWatchList configWatchList,GoConfigPluginService configPluginService, ServerHealthService healthService) { this.configPluginService = configPluginService; this.serverHealthService = healthService; this.configWatchList = configWatchList; this.configWatchList.registerListener(this); } public boolean hasListener(PartialConfigUpdateCompletedListener listener) { return this.listeners.contains(listener); } public void registerListener(PartialConfigUpdateCompletedListener listener) { this.listeners.add(listener); } public boolean latestParseHasFailedForMaterial(MaterialConfig material) { String fingerprint = material.getFingerprint(); PartialConfigParseResult result = fingerprintOfPartialToLatestParseResultMap.get(fingerprint); if (result == null) return false; return result.getLastFailure() != null; } public PartialConfig latestPartialConfigForMaterial(MaterialConfig material) throws Exception { String fingerprint = material.getFingerprint(); PartialConfigParseResult result = fingerprintOfPartialToLatestParseResultMap.get(fingerprint); if(result == null) return null; if(result.getLastFailure() != null) throw result.getLastFailure(); return result.getLastSuccess(); } @Override public void onChangedRepoConfigWatchList(ConfigReposConfig newConfigRepos) { // remove partial configs from map which are no longer on the list for(String fingerprint : this.fingerprintOfPartialToLatestParseResultMap.keySet()) { if(!newConfigRepos.hasMaterialWithFingerprint(fingerprint)) { this.fingerprintOfPartialToLatestParseResultMap.remove(fingerprint); } } } public void onCheckoutComplete(MaterialConfig material, File folder, String revision) { // called when pipelines/flyweight/[flyweight] has a clean checkout of latest material // Having modifications in signature might seem like an overkill // but on the other hand if plugin is smart enough it could // parse only files that have changed, which is a huge performance gain where there are many pipelines /* if this is material listed in config-repos Then ask for config plugin implementation Give it the directory and store partial config post event about completed (successful or not) parsing */ String fingerprint = material.getFingerprint(); if(this.configWatchList.hasConfigRepoWithFingerprint(fingerprint)) { PartialConfigProvider plugin = null; ConfigRepoConfig repoConfig = configWatchList.getConfigRepoForMaterial(material); HealthStateScope scope = HealthStateScope.forPartialConfigRepo(repoConfig); try { plugin = this.configPluginService.partialConfigProviderFor(repoConfig); } catch (Exception ex) { fingerprintOfPartialToLatestParseResultMap.put(fingerprint, new PartialConfigParseResult(revision,ex)); LOGGER.error(String.format("Failed to get config plugin for %s", material.getDisplayName())); String message = String.format("Failed to obtain configuration plugin '%s' for material: %s", repoConfig.getConfigProviderPluginName(), material.getLongDescription()); String errorDescription = ex.getMessage() == null ? ex.toString() : ex.getMessage(); serverHealthService.update(ServerHealthState.error(message, errorDescription, HealthStateType.general(scope))); notifyFailureListeners(repoConfig, ex); return; } try { //TODO put modifications and previous partial config in context // the context is just a helper for plugin. PartialConfigLoadContext context = new LoadContext(repoConfig); PartialConfig newPart = plugin.load(folder, context); if(newPart == null) { LOGGER.warn(String.format("Parsed configuration material %s by %s is null", material.getDisplayName(), plugin.displayName())); newPart = new PartialConfig(); } newPart.setOrigins(new RepoConfigOrigin(repoConfig,revision)); fingerprintOfPartialToLatestParseResultMap.put(fingerprint, new PartialConfigParseResult(revision,newPart)); serverHealthService.removeByScope(scope); notifySuccessListeners(repoConfig, newPart); } catch (Exception ex) { fingerprintOfPartialToLatestParseResultMap.put(fingerprint, new PartialConfigParseResult(revision,ex)); LOGGER.error(String.format("Failed to parse configuration material %s by %s", material.getDisplayName(),plugin.displayName()), ex); String message = String.format("Parsing configuration repository using %s failed for material: %s", plugin.displayName(), material.getLongDescription()); String errorDescription = ex.getMessage() == null ? ex.toString() : ex.getMessage(); serverHealthService.update(ServerHealthState.error(message, errorDescription, HealthStateType.general(scope))); notifyFailureListeners(repoConfig, ex); } } } private void notifyFailureListeners(ConfigRepoConfig repoConfig, Exception ex) { for (PartialConfigUpdateCompletedListener listener : this.listeners) { try { listener.onFailedPartialConfig(repoConfig, ex); } catch (Exception e) { LOGGER.error(String.format("Failed to fire event 'exception while parsing partial configuration' for listener %s", listener)); } } } private void notifySuccessListeners(ConfigRepoConfig repoConfig, PartialConfig newPart) { for (PartialConfigUpdateCompletedListener listener : this.listeners) { try { listener.onSuccessPartialConfig(repoConfig, newPart); } catch (Exception e) { LOGGER.error(String.format("Failed to fire parsed partial configuration for listener %s", listener)); } } } public String getRevisionAtLastAttempt(MaterialConfig material) { String fingerprint = material.getFingerprint(); PartialConfigParseResult result = fingerprintOfPartialToLatestParseResultMap.get(fingerprint); if (result == null) return null; return result.getRevision(); } private class PartialConfigParseResult{ private final String revision; private PartialConfig lastSuccess; private Exception lastFailure; public PartialConfigParseResult(String revision, PartialConfig newPart) { this.revision = revision; this.lastSuccess = newPart; } public PartialConfigParseResult(String revision,Exception ex) { this.revision = revision; this.lastFailure = ex; } public PartialConfig getLastSuccess() { return lastSuccess; } public void setLastSuccess(PartialConfig lastSuccess) { this.lastSuccess = lastSuccess; } public Exception getLastFailure() { return lastFailure; } public void setLastFailure(Exception lastFailure) { this.lastFailure = lastFailure; } public String getRevision() { return revision; } } private class LoadContext implements PartialConfigLoadContext { private ConfigRepoConfig repoConfig; public LoadContext(ConfigRepoConfig repoConfig) { this.repoConfig = repoConfig; } @Override public Configuration configuration() { return repoConfig.getConfiguration(); } @Override public MaterialConfig configMaterial() { return this.repoConfig.getMaterialConfig(); } } }