/* * Copyright 2017 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. */ package com.thoughtworks.go.config; import com.rits.cloning.Cloner; import com.thoughtworks.go.config.materials.MaterialConfigs; import com.thoughtworks.go.config.materials.ScmMaterialConfig; import com.thoughtworks.go.config.materials.git.GitMaterialConfig; import com.thoughtworks.go.config.materials.mercurial.HgMaterialConfig; import com.thoughtworks.go.config.parts.XmlPartialConfigProvider; import com.thoughtworks.go.config.remote.ConfigRepoConfig; import com.thoughtworks.go.config.remote.PartialConfig; import com.thoughtworks.go.config.remote.RepoConfigOrigin; import com.thoughtworks.go.config.update.CreatePipelineConfigCommand; import com.thoughtworks.go.config.update.FullConfigUpdateCommand; import com.thoughtworks.go.config.validation.GoConfigValidity; import com.thoughtworks.go.domain.materials.MaterialConfig; import com.thoughtworks.go.helper.*; import com.thoughtworks.go.listener.ConfigChangedListener; import com.thoughtworks.go.server.domain.Username; import com.thoughtworks.go.server.service.GoConfigService; import com.thoughtworks.go.server.service.result.DefaultLocalizedOperationResult; 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 com.thoughtworks.go.service.ConfigRepository; import com.thoughtworks.go.util.*; import com.thoughtworks.go.util.command.CommandLine; import com.thoughtworks.go.util.command.ConsoleResult; import org.apache.commons.io.FileUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.*; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.thoughtworks.go.helper.ConfigFileFixture.DEFAULT_XML_WITH_2_AGENTS; import static java.util.Arrays.asList; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:WEB-INF/applicationContext-global.xml", "classpath:WEB-INF/applicationContext-dataLocalAccess.xml", "classpath:WEB-INF/applicationContext-acegi-security.xml" }) public class CachedGoConfigIntegrationTest { @Autowired private GoConfigWatchList configWatchList; @Autowired private GoRepoConfigDataSource repoConfigDataSource; @Autowired private CachedGoConfig cachedGoConfig; private GoConfigFileHelper configHelper; @Autowired private ServerHealthService serverHealthService; @Autowired private GoConfigService goConfigService; @Autowired private GoConfigDao goConfigDao; @Autowired private CachedGoPartials cachedGoPartials; @Autowired private GoPartialConfig goPartialConfig; @Autowired private GoFileConfigDataSource goFileConfigDataSource; @Autowired private ConfigRepository configRepository; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); private String latestCommit; private ConfigRepoConfig configRepo; private File externalConfigRepo; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() throws Exception { configHelper = new GoConfigFileHelper(DEFAULT_XML_WITH_2_AGENTS); configHelper.usingCruiseConfigDao(goConfigDao).initializeConfigFile(); configHelper.onSetUp(); externalConfigRepo = temporaryFolder.newFolder(); latestCommit = setupExternalConfigRepo(externalConfigRepo); configHelper.addConfigRepo(new ConfigRepoConfig(new GitMaterialConfig(externalConfigRepo.getAbsolutePath()), XmlPartialConfigProvider.providerName)); goConfigService.forceNotifyListeners(); configRepo = configWatchList.getCurrentConfigRepos().get(0); cachedGoPartials.clear(); configHelper.addAgent("hostname1", "uuid1"); } @After public void tearDown() throws Exception { cachedGoPartials.clear(); for (PartialConfig partial : cachedGoPartials.lastValidPartials()) { assertThat(ErrorCollector.getAllErrors(partial).isEmpty(), is(true)); } for (PartialConfig partial : cachedGoPartials.lastKnownPartials()) { assertThat(ErrorCollector.getAllErrors(partial).isEmpty(), is(true)); } } @Test public void shouldRecoverFromDeepConfigRepoReferencesBug1901When2Repos() throws Exception { // pipeline references are like this: pipe1 -> downstream File downstreamExternalConfigRepo = temporaryFolder.newFolder(); /*here is a pipeline 'downstream' with material dependency on 'pipe1' in other repository*/ String downstreamLatestCommit = setupExternalConfigRepo(downstreamExternalConfigRepo, "external_git_config_repo_referencing_first"); configHelper.addConfigRepo(new ConfigRepoConfig(new GitMaterialConfig(downstreamExternalConfigRepo.getAbsolutePath()), "gocd-xml")); goConfigService.forceNotifyListeners();//TODO what if this is not called? ConfigRepoConfig downstreamConfigRepo = configWatchList.getCurrentConfigRepos().get(1); assertThat(configWatchList.getCurrentConfigRepos().size(), is(2)); // And unluckily downstream gets parsed first repoConfigDataSource.onCheckoutComplete(downstreamConfigRepo.getMaterialConfig(), downstreamExternalConfigRepo, downstreamLatestCommit); // So parsing fails and proper message is shown: List<ServerHealthState> messageForInvalidMerge = serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(downstreamConfigRepo)); assertThat(messageForInvalidMerge.isEmpty(), is(false)); assertThat(messageForInvalidMerge.get(0).getDescription(), containsString("tries to fetch artifact from pipeline "pipe1"")); // and current config is still old assertThat(goConfigService.hasPipelineNamed(new CaseInsensitiveString("downstream")), is(false)); assertThat(cachedGoPartials.lastKnownPartials().size(), is(1)); assertThat(cachedGoPartials.lastValidPartials().size(), is(0)); //here downstream partial is waiting to be merged assertThat(cachedGoPartials.lastKnownPartials().get(0).getGroups().get(0).hasPipeline(new CaseInsensitiveString("downstream")), is(true)); // Finally upstream config repository is parsed repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); // now server should be healthy and contain all pipelines assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(configRepo)).isEmpty(), is(true)); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(downstreamConfigRepo)).isEmpty(), is(true)); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("downstream")), is(true)); } @Test public void shouldRecoverFromDeepConfigRepoReferencesBug1901When3Repos() throws Exception { // pipeline references are like this: pipe1 -> downstream -> downstream2 File secondDownstreamExternalConfigRepo = temporaryFolder.newFolder(); /*here is a pipeline 'downstream2' with material dependency on 'downstream' in other repository*/ String secondDownstreamLatestCommit = setupExternalConfigRepo(secondDownstreamExternalConfigRepo, "external_git_config_repo_referencing_second"); configHelper.addConfigRepo(new ConfigRepoConfig(new GitMaterialConfig(secondDownstreamExternalConfigRepo.getAbsolutePath()), "gocd-xml")); File firstDownstreamExternalConfigRepo = temporaryFolder.newFolder(); /*here is a pipeline 'downstream' with material dependency on 'pipe1' in other repository*/ String firstDownstreamLatestCommit = setupExternalConfigRepo(firstDownstreamExternalConfigRepo, "external_git_config_repo_referencing_first"); configHelper.addConfigRepo(new ConfigRepoConfig(new GitMaterialConfig(firstDownstreamExternalConfigRepo.getAbsolutePath()), "gocd-xml")); goConfigService.forceNotifyListeners(); ConfigRepoConfig firstDownstreamConfigRepo = configWatchList.getCurrentConfigRepos().get(1); ConfigRepoConfig secondDownstreamConfigRepo = configWatchList.getCurrentConfigRepos().get(2); assertThat(configWatchList.getCurrentConfigRepos().size(), is(3)); // And unluckily downstream2 gets parsed first repoConfigDataSource.onCheckoutComplete(secondDownstreamConfigRepo.getMaterialConfig(), secondDownstreamExternalConfigRepo, secondDownstreamLatestCommit); // So parsing fails and proper message is shown: List<ServerHealthState> messageForInvalidMerge = serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(secondDownstreamConfigRepo)); assertThat(messageForInvalidMerge.isEmpty(), is(false)); assertThat(messageForInvalidMerge.get(0).getDescription(), containsString("tries to fetch artifact from pipeline "downstream"")); // and current config is still old assertThat(goConfigService.hasPipelineNamed(new CaseInsensitiveString("downstream2")), is(false)); assertThat(cachedGoPartials.lastKnownPartials().size(), is(1)); assertThat(cachedGoPartials.lastValidPartials().size(), is(0)); //here downstream2 partial is waiting to be merged assertThat(cachedGoPartials.lastKnownPartials().get(0).getGroups().get(0).hasPipeline(new CaseInsensitiveString("downstream2")), is(true)); // Then middle upstream config repository is parsed repoConfigDataSource.onCheckoutComplete(firstDownstreamConfigRepo.getMaterialConfig(), firstDownstreamExternalConfigRepo, firstDownstreamLatestCommit); // and errors are still shown messageForInvalidMerge = serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(firstDownstreamConfigRepo)); assertThat(messageForInvalidMerge.isEmpty(), is(false)); assertThat(messageForInvalidMerge.get(0).getDescription(), containsString("Pipeline "pipe1" does not exist. It is used from pipeline "downstream"")); // and current config is still old assertThat(goConfigService.hasPipelineNamed(new CaseInsensitiveString("downstream")), is(false)); assertThat(goConfigService.hasPipelineNamed(new CaseInsensitiveString("downstream2")), is(false)); assertThat(cachedGoPartials.lastKnownPartials().size(), is(2)); assertThat(cachedGoPartials.lastValidPartials().size(), is(0)); // Finally upstream config repository is parsed repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); // now server should be healthy and contain all pipelines assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(firstDownstreamConfigRepo)).isEmpty(), is(true)); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(secondDownstreamConfigRepo)).isEmpty(), is(true)); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("downstream")), is(true)); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("downstream2")), is(true)); } @Test public void shouldFailWhenTryingToAddPipelineDefinedRemotely() throws Exception { assertThat(configWatchList.getCurrentConfigRepos().size(), is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(cachedGoConfig.loadMergedForEditing().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); PipelineConfig dupPipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("pipe1", "ut", "www.spring.com"); try { goConfigDao.addPipeline(dupPipelineConfig, PipelineConfigs.DEFAULT_GROUP); } catch (RuntimeException ex) { assertThat(ex.getMessage(), containsString("You have defined multiple pipelines named 'pipe1'. Pipeline names must be unique. Source(s):")); return; } fail("Should have thrown"); } @Test public void shouldNotifyListenersWhenConfigChanged() { ConfigChangeListenerStub listener = new ConfigChangeListenerStub(); cachedGoConfig.registerListener(listener); assertThat(listener.invocationCount, is(1)); cachedGoConfig.writeWithLock(new UpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { return cruiseConfig; } }); assertThat(listener.invocationCount, is(2)); } @Test public void shouldReturnMergedConfig_WhenThereIsValidPartialConfig() throws Exception { assertThat(configWatchList.getCurrentConfigRepos().size(), is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(configRepo)).isEmpty(), is(true)); assertThat(repoConfigDataSource.latestPartialConfigForMaterial(configRepo.getMaterialConfig()).getGroups().findGroup("first").findBy(new CaseInsensitiveString("pipe1")), is(not(nullValue()))); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); } @Test public void shouldFailWhenTryingToAddPipelineWithTheSameNameAsAnotherPipelineDefinedRemotely_EntitySave() throws Exception { assertThat(configWatchList.getCurrentConfigRepos().size(), is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); PipelineConfig dupPipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("pipe1", "ut", "www.spring.com"); try { goConfigDao.updateConfig(new CreatePipelineConfigCommand(goConfigService, dupPipelineConfig, Username.ANONYMOUS, new DefaultLocalizedOperationResult(), "default"), Username.ANONYMOUS); fail("Should have thrown"); } catch (RuntimeException ex) { PipelineConfig pipe1 = goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipe1")); String errorMessage = dupPipelineConfig.errors().on(PipelineConfig.NAME); assertThat(errorMessage, containsString("You have defined multiple pipelines named 'pipe1'. Pipeline names must be unique. Source(s):")); Matcher matcher = Pattern.compile("^.*\\[(.*),\\s(.*)\\].*$").matcher(errorMessage); assertThat(matcher.matches(), is(true)); assertThat(matcher.groupCount(), is(2)); List<String> expectedSources = asList(dupPipelineConfig.getOriginDisplayName(), pipe1.getOriginDisplayName()); List<String> actualSources = new ArrayList<>(); for (int i = 1; i <= matcher.groupCount(); i++) { actualSources.add(matcher.group(i)); } assertThat(actualSources.size(), is(expectedSources.size())); assertThat(actualSources.containsAll(expectedSources), is(true)); } } @Test public void shouldFailWhenTryingToAddPipelineWithTheSameNameAsAnotherPipelineDefinedRemotely_FullConfigSave() throws Exception { assertThat(configWatchList.getCurrentConfigRepos().size(), is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); final PipelineConfig dupPipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("pipe1", "ut", "www.spring.com"); try { goConfigDao.updateConfig(new UpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getGroups().first().add(dupPipelineConfig); return cruiseConfig; } }); fail("Should have thrown"); } catch (RuntimeException ex) { String errorMessage = ex.getMessage(); assertThat(errorMessage, containsString("You have defined multiple pipelines named 'pipe1'. Pipeline names must be unique. Source(s):")); Matcher matcher = Pattern.compile("^.*\\[(.*),\\s(.*)\\].*$").matcher(errorMessage); assertThat(matcher.matches(), is(true)); assertThat(matcher.groupCount(), is(2)); PipelineConfig pipe1 = goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipe1")); List<String> expectedSources = asList(dupPipelineConfig.getOriginDisplayName(), pipe1.getOriginDisplayName()); List<String> actualSources = new ArrayList<>(); for (int i = 1; i <= matcher.groupCount(); i++) { actualSources.add(matcher.group(i)); } assertThat(actualSources.size(), is(expectedSources.size())); assertThat(actualSources.containsAll(expectedSources), is(true)); } } @Test public void shouldReturnRemotePipelinesAmongAllPipelinesInMergedConfigForEdit() throws Exception { assertThat(configWatchList.getCurrentConfigRepos().size(), is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(cachedGoConfig.loadMergedForEditing().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); } private ArrayList<ServerHealthState> findMessageFor(final HealthStateType type) { return ListUtil.filterInto(new ArrayList<>(), serverHealthService.getAllLogs(), new Filter<ServerHealthState>() { @Override public boolean matches(ServerHealthState element) { boolean b = element.getType().equals(type); return b; } }); } @Test public void shouldNotifyWithMergedConfig_WhenPartUpdated() throws Exception { ConfigChangeListenerStub listener = new ConfigChangeListenerStub(); cachedGoConfig.registerListener(listener); // at registration assertThat(listener.invocationCount, is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat("currentConfigShouldBeMerged", cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipe1")), is(true)); assertThat(listener.invocationCount, is(2)); } @Test public void shouldNotNotifyListenersWhenMergeFails() throws IOException { checkinPartial("config_repo_with_invalid_partial"); ConfigRepoConfig configRepo = configWatchList.getCurrentConfigRepos().get(0); ConfigChangeListenerStub listener = new ConfigChangeListenerStub(); cachedGoConfig.registerListener(listener); // at registration assertThat(listener.invocationCount, is(1)); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat("currentConfigShouldBeMainXmlOnly", cachedGoConfig.currentConfig().hasPipelineNamed(new CaseInsensitiveString("pipeline_with_no_stage")), is(false)); assertThat(listener.invocationCount, is(1)); } @Test public void shouldSetErrorHealthStateWhenMergeFails() throws IOException { checkinPartial("config_repo_with_invalid_partial"); ConfigRepoConfig configRepo = configWatchList.getCurrentConfigRepos().get(0); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); List<ServerHealthState> messageForInvalidMerge = serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(configRepo)); assertThat(messageForInvalidMerge.isEmpty(), is(false)); assertThat(messageForInvalidMerge.get(0).getDescription().contains("Pipeline 'pipeline_with_no_stage' does not have any stages configured"), is(true)); } @Test public void shouldUnSetErrorHealthStateWhenMergePasses() throws IOException { ConfigRepoConfig configRepo = configWatchList.getCurrentConfigRepos().get(0); checkinPartial("config_repo_with_invalid_partial/bad_partial.gocd.xml"); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(configRepo)).isEmpty(), is(false)); //fix partial deletePartial("bad_partial.gocd.xml"); repoConfigDataSource.onCheckoutComplete(configRepo.getMaterialConfig(), externalConfigRepo, latestCommit); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(configRepo)).isEmpty(), is(true)); } @Test public void shouldUpdateCachedConfigOnSave() throws Exception { assertThat(cachedGoConfig.currentConfig().agents().size(), Matchers.is(1)); configHelper.addAgent("hostname", "uuid2"); assertThat(cachedGoConfig.currentConfig().agents().size(), Matchers.is(2)); } @Test public void shouldReloadCachedConfigWhenWriting() throws Exception { cachedGoConfig.writeWithLock(updateFirstAgentResources("osx")); assertThat(cachedGoConfig.currentConfig().agents().get(0).getResources().toString(), Matchers.is("osx")); cachedGoConfig.writeWithLock(updateFirstAgentResources("osx, firefox")); assertThat(cachedGoConfig.currentConfig().agents().get(0).getResources().toString(), Matchers.is("firefox | osx")); } @Test public void shouldReloadCachedConfigFromDisk() throws Exception { assertThat(cachedGoConfig.currentConfig().agents().size(), Matchers.is(1)); configHelper.writeXmlToConfigFile(ConfigFileFixture.TASKS_WITH_CONDITION); cachedGoConfig.forceReload(); assertThat(cachedGoConfig.currentConfig().agents().size(), Matchers.is(0)); } @Test public void shouldInterpolateParamsInTemplate() throws Exception { String content = "<cruise schemaVersion='" + GoConstants.CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' >" + "</server>" + "<pipelines>\n" + "<pipeline name='dev' template='abc'>\n" + " <params>" + " <param name='command'>ls</param>" + " <param name='dir'>/tmp</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "<pipeline name='acceptance' template='abc'>\n" + " <params>" + " <param name='command'>twist</param>" + " <param name='dir'>./acceptance</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + " <pipeline name='abc'>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1'>" + " <tasks>" + " <exec command='/bin/#{command}' args='#{dir}'/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>\n" + "</templates>\n" + "</cruise>"; configHelper.writeXmlToConfigFile(content); cachedGoConfig.forceReload(); CruiseConfig cruiseConfig = cachedGoConfig.currentConfig(); ExecTask devExec = (ExecTask) cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("dev")).getFirstStageConfig().jobConfigByConfigName(new CaseInsensitiveString("job1")).getTasks().first(); assertThat(devExec, Is.is(new ExecTask("/bin/ls", "/tmp", (String) null))); ExecTask acceptanceExec = (ExecTask) cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("acceptance")).getFirstStageConfig().jobConfigByConfigName(new CaseInsensitiveString("job1")).getTasks().first(); assertThat(acceptanceExec, Is.is(new ExecTask("/bin/twist", "./acceptance", (String) null))); cruiseConfig = cachedGoConfig.loadForEditing(); devExec = (ExecTask) cruiseConfig.getTemplateByName(new CaseInsensitiveString("abc")).get(0).jobConfigByConfigName(new CaseInsensitiveString("job1")).getTasks().first(); assertThat(devExec, Is.is(new ExecTask("/bin/#{command}", "#{dir}", (String) null))); assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("dev")).size(), Matchers.is(0)); assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("acceptance")).size(), Matchers.is(0)); } @Test public void shouldHandleParamQuotingCorrectly() throws Exception { String content = "<cruise schemaVersion='" + GoConstants.CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='dev'>\n" + " <params>" + " <param name='command'>ls#{a}</param>" + " <param name='dir'>/tmp</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1'>" + " <tasks>" + " <exec command='/bin/#{command}##{b}' args='#{dir}'/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; configHelper.writeXmlToConfigFile(content); cachedGoConfig.forceReload(); CruiseConfig cruiseConfig = cachedGoConfig.currentConfig(); ExecTask devExec = (ExecTask) cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("dev")).getFirstStageConfig().jobConfigByConfigName(new CaseInsensitiveString("job1")).getTasks().first(); assertThat(devExec, Is.is(new ExecTask("/bin/ls#{a}#{b}", "/tmp", (String) null))); } @Test public void shouldAllowParamsInLabelTemplates() throws Exception { String content = "<cruise schemaVersion='" + GoConstants.CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='dev' labeltemplate='cruise-#{VERSION}-${COUNT}'>\n" + " <params>" + " <param name='VERSION'>1.2</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1'>" + " <tasks>" + " <exec command='/bin/ls' args='some'/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; configHelper.writeXmlToConfigFile(content); cachedGoConfig.forceReload(); CruiseConfig cruiseConfig = cachedGoConfig.currentConfig(); assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("dev")).getLabelTemplate(), Is.is("cruise-1.2-${COUNT}")); } @Test public void shouldThrowErrorWhenEnvironmentVariablesAreDuplicate() throws Exception { String content = "<cruise schemaVersion='" + GoConstants.CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='dev'>\n" + " <params>" + " <param name='product'>GO</param>" + " </params>" + " <environmentvariables>" + " <variable name='#{product}_WORKING_DIR'><value>go_dir</value></variable>" + " <variable name='GO_WORKING_DIR'><value>dir</value></variable>" + " </environmentvariables>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1'>" + " <tasks>" + " <exec command='/bin/ls' args='some'/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; configHelper.writeXmlToConfigFile(content); GoConfigValidity configValidity = cachedGoConfig.checkConfigFileValid(); assertThat(configValidity.isValid(), Matchers.is(false)); assertThat(configValidity.errorMessage(), containsString("Environment Variable name 'GO_WORKING_DIR' is not unique for pipeline 'dev'")); } @Test public void shouldReturnCachedConfigIfConfigFileIsInvalid() throws Exception { CruiseConfig before = cachedGoConfig.currentConfig(); assertThat(before.agents().size(), Matchers.is(1)); configHelper.writeXmlToConfigFile("invalid-xml"); cachedGoConfig.forceReload(); assertTrue(cachedGoConfig.currentConfig() == before); assertThat(cachedGoConfig.checkConfigFileValid().isValid(), Matchers.is(false)); } @Test public void shouldClearInvalidExceptionWhenConfigErrorsAreFixed() throws Exception { configHelper.writeXmlToConfigFile("invalid-xml"); cachedGoConfig.forceReload(); cachedGoConfig.currentConfig(); assertThat(cachedGoConfig.checkConfigFileValid().isValid(), Matchers.is(false)); configHelper.addAgent("hostname", "uuid2");//some valid change CruiseConfig cruiseConfig = cachedGoConfig.currentConfig(); assertThat(cruiseConfig.agents().size(), Matchers.is(2)); assertThat(cachedGoConfig.checkConfigFileValid().isValid(), Matchers.is(true)); } @Test public void shouldSetServerHealthMessageWhenConfigFileIsInvalid() throws IOException { configHelper.writeXmlToConfigFile("invalid-xml"); cachedGoConfig.forceReload(); assertThat(cachedGoConfig.checkConfigFileValid().isValid(), Matchers.is(false)); List<ServerHealthState> serverHealthStates = serverHealthService.getAllLogs(); assertThat(serverHealthStates.isEmpty(), is(false)); assertThat(serverHealthStates.contains(ServerHealthState.error(GoConfigService.INVALID_CRUISE_CONFIG_XML, "Error on line 1: Content is not allowed in prolog.", HealthStateType.invalidConfig())), is(true)); } @Test public void shouldClearServerHealthMessageWhenConfigFileIsValid() throws IOException { serverHealthService.update(ServerHealthState.error(GoConfigService.INVALID_CRUISE_CONFIG_XML, "Error on line 1: Content is not allowed in prolog.", HealthStateType.invalidConfig())); Assert.assertThat(findMessageFor(HealthStateType.invalidConfig()).isEmpty(), is(false)); configHelper.writeXmlToConfigFile(ConfigFileFixture.TASKS_WITH_CONDITION); cachedGoConfig.forceReload(); Assert.assertThat(cachedGoConfig.checkConfigFileValid().isValid(), Matchers.is(true)); Assert.assertThat(findMessageFor(HealthStateType.invalidConfig()).isEmpty(), is(true)); } @Test public void shouldReturnDefaultCruiseConfigIfLoadingTheConfigFailsForTheFirstTime() throws Exception { ReflectionUtil.setField(cachedGoConfig, "currentConfig", null); configHelper.writeXmlToConfigFile("invalid-xml"); Assert.assertThat(cachedGoConfig.currentConfig(), Matchers.<CruiseConfig>is(new BasicCruiseConfig())); } @Test public void shouldGetConfigForEditAndRead() throws Exception { CruiseConfig cruiseConfig = configHelper.load(); addPipelineWithParams(cruiseConfig); configHelper.writeConfigFile(cruiseConfig); PipelineConfig config = cachedGoConfig.currentConfig().pipelineConfigByName(new CaseInsensitiveString("mingle")); HgMaterialConfig hgMaterialConfig = (HgMaterialConfig) byFolder(config.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://hg-server/repo-name")); config = cachedGoConfig.loadForEditing().pipelineConfigByName(new CaseInsensitiveString("mingle")); hgMaterialConfig = (HgMaterialConfig) byFolder(config.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://#{foo}/#{bar}")); } @Test public void shouldLoadConfigForReadAndEditWhenNewXMLIsWritten() throws Exception { String pipelineName = "mingle"; cachedGoConfig.save(configXmlWithPipeline(pipelineName), false); PipelineConfig reloadedPipelineConfig = cachedGoConfig.currentConfig().pipelineConfigByName(new CaseInsensitiveString(pipelineName)); HgMaterialConfig hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://hg-server/repo-name")); reloadedPipelineConfig = cachedGoConfig.loadForEditing().pipelineConfigByName(new CaseInsensitiveString(pipelineName)); hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://#{foo}/#{bar}")); GoConfigHolder configHolder = cachedGoConfig.loadConfigHolder(); reloadedPipelineConfig = configHolder.config.pipelineConfigByName(new CaseInsensitiveString(pipelineName)); hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://hg-server/repo-name")); reloadedPipelineConfig = configHolder.configForEdit.pipelineConfigByName(new CaseInsensitiveString(pipelineName)); hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://#{foo}/#{bar}")); } @Test public void shouldLoadConfigForReadAndEditWhenConfigIsUpdatedThoughACommand() throws Exception { cachedGoConfig.writeWithLock(new UpdateConfigCommand() { public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { addPipelineWithParams(cruiseConfig); return cruiseConfig; } }); PipelineConfig reloadedPipelineConfig = cachedGoConfig.currentConfig().pipelineConfigByName(new CaseInsensitiveString("mingle")); HgMaterialConfig hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://hg-server/repo-name")); reloadedPipelineConfig = cachedGoConfig.loadForEditing().pipelineConfigByName(new CaseInsensitiveString("mingle")); hgMaterialConfig = (HgMaterialConfig) byFolder(reloadedPipelineConfig.materialConfigs(), "folder"); Assert.assertThat(hgMaterialConfig.getUrl(), Matchers.is("http://#{foo}/#{bar}")); } private String configXmlWithPipeline(String pipelineName) { return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion=\"" + GoConstants.CONFIG_SCHEMA_VERSION + "\">\n" + " <server artifactsdir=\"artifactsDir\" serverId=\"dd8d0f5a-7e8d-4948-a1c7-ddcedbac15d0\" />\n" + " <pipelines group=\"another\">\n" + " <pipeline name=\"" + pipelineName + "\">\n" + " <params>\n" + " <param name=\"foo\">hg-server</param>\n" + " <param name=\"bar\">repo-name</param>\n" + " </params>\n" + " <materials>\n" + " <svn url=\"http://some/svn/url\" dest=\"svnDir\" materialName=\"url\" />\n" + " <hg url=\"http://#{foo}/#{bar}\" dest=\"folder\" />\n" + " </materials>\n" + " <stage name=\"dev\">\n" + " <jobs>\n" + " <job name=\"ant\" />\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + " </pipelines>\n" + "</cruise>\n" + "\n"; } @Test public void shouldReturnUpdatedStatusWhenConfigIsUpdatedWithLatestCopy() { final String md5 = cachedGoConfig.currentConfig().getMd5(); ConfigSaveState firstSaveState = cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.addPipeline("g1", PipelineConfigMother.createPipelineConfig("p1", "s1", "j1")); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); assertThat(firstSaveState, is(ConfigSaveState.UPDATED)); } @Test public void shouldReturnMergedStatusWhenConfigIsMergedWithStaleCopy() { final String md5 = cachedGoConfig.currentConfig().getMd5(); ConfigSaveState firstSaveState = cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.addPipeline("g1", PipelineConfigMother.createPipelineConfig("p1", "s1", "j1")); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); assertThat(firstSaveState, is(ConfigSaveState.UPDATED)); ConfigSaveState secondSaveState = cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.server().setArtifactsDir("something"); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); assertThat(secondSaveState, is(ConfigSaveState.MERGED)); } @Test public void shouldNotAllowAGitMergeOfConcurrentChangesIfTheChangeCausesMergedPartialsToBecomeInvalid() { final String upstream = UUID.randomUUID().toString(); String remoteDownstream = "remote-downstream"; setupExternalConfigRepoWithDependencyMaterialOnPipelineInMainXml(upstream, remoteDownstream); final String md5 = cachedGoConfig.currentConfig().getMd5(); // some random unrelated change to force a git merge workflow cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.server().setCommandRepositoryLocation("new_location"); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); thrown.expectMessage(String.format("Stage with name 'stage' does not exist on pipeline '%s', it is being referred to from pipeline 'remote-downstream' (%s at r1)", upstream, configRepo.getMaterialConfig().getDisplayName())); cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getPipelineConfigByName(new CaseInsensitiveString(upstream)).getFirstStageConfig().setName(new CaseInsensitiveString("new_name")); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); } @Test public void shouldMarkAPartialAsValidIfItBecomesValidBecauseOfNewerChangesInMainXml_GitMergeWorkflow() { final String upstream = UUID.randomUUID().toString(); String remoteDownstream = "remote-downstream"; setupExternalConfigRepoWithDependencyMaterialOnPipelineInMainXml(upstream, remoteDownstream); PartialConfig partialWithStageRenamed = new Cloner().deepClone(goPartialConfig.lastPartials().get(0)); PipelineConfig pipelineInRemoteConfigRepo = partialWithStageRenamed.getGroups().get(0).getPipelines().get(0); pipelineInRemoteConfigRepo.materialConfigs().getDependencyMaterial().setStageName(new CaseInsensitiveString("new_name")); partialWithStageRenamed.setOrigin(new RepoConfigOrigin(configRepo, "r2")); goPartialConfig.onSuccessPartialConfig(configRepo, partialWithStageRenamed); final String md5 = cachedGoConfig.currentConfig().getMd5(); // some random unrelated change to force a git merge workflow cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.server().setCommandRepositoryLocation("new_location"); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); ConfigSaveState saveState = cachedGoConfig.writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getPipelineConfigByName(new CaseInsensitiveString(upstream)).getFirstStageConfig().setName(new CaseInsensitiveString("new_name")); return cruiseConfig; } @Override public String unmodifiedMd5() { return md5; } }); assertThat(saveState, is(ConfigSaveState.MERGED)); assertThat(cachedGoPartials.lastValidPartials().get(0).getGroups().first().get(0).materialConfigs().getDependencyMaterial().getStageName(), is(new CaseInsensitiveString("new_name"))); assertThat(goConfigService.getConfigForEditing().getPipelineConfigByName(new CaseInsensitiveString(upstream)).getFirstStageConfig().name(), is(new CaseInsensitiveString("new_name"))); assertThat(goConfigService.getCurrentConfig().getPipelineConfigByName(new CaseInsensitiveString(upstream)).getFirstStageConfig().name(), is(new CaseInsensitiveString("new_name"))); } private void setupExternalConfigRepoWithDependencyMaterialOnPipelineInMainXml(String upstream, String remoteDownstreamPipelineName) { PipelineConfig upstreamPipelineConfig = GoConfigMother.createPipelineConfigWithMaterialConfig(upstream, new GitMaterialConfig("FOO")); goConfigService.addPipeline(upstreamPipelineConfig, "default"); PartialConfig partialConfig = PartialConfigMother.pipelineWithDependencyMaterial(remoteDownstreamPipelineName, upstreamPipelineConfig, new RepoConfigOrigin(configRepo, "r1")); goPartialConfig.onSuccessPartialConfig(configRepo, partialConfig); } @Test public void shouldSaveConfigChangesWhenFullConfigIsBeingSavedFromConfigXmlTabAndAllKnownConfigRepoPartialsAreInvalid() throws Exception { cachedGoPartials.clear(); PartialConfig invalidPartial = PartialConfigMother.invalidPartial("invalid", new RepoConfigOrigin(configRepo, "revision1")); goPartialConfig.onSuccessPartialConfig(configRepo, invalidPartial); CruiseConfig updatedConfig = new Cloner().deepClone(goConfigService.getConfigForEditing()); updatedConfig.server().setCommandRepositoryLocation("foo"); String updatedXml = goFileConfigDataSource.configAsXml(updatedConfig, false); FileUtils.writeStringToFile(new File(goConfigDao.fileLocation()), updatedXml); GoConfigValidity validity = goConfigService.fileSaver(false).saveXml(updatedXml, goConfigDao.md5OfConfigFile()); assertThat(validity.isValid(), is(true)); assertThat(cachedGoPartials.lastValidPartials().isEmpty(), is(true)); assertThat(cachedGoPartials.lastKnownPartials().contains(invalidPartial), is(true)); } @Test public void shouldAllowFallbackMergeAndSaveWhenKnownPartialHasAnInvalidEnvironmentThatRefersToAnUnknownPipeline() throws Exception { cachedGoPartials.clear(); PartialConfig partialConfigWithInvalidEnvironment = PartialConfigMother.withEnvironment("env", new RepoConfigOrigin(configRepo, "revision1")); goPartialConfig.onSuccessPartialConfig(configRepo, partialConfigWithInvalidEnvironment); ConfigSaveState state = cachedGoConfig.writeWithLock(new UpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.server().setCommandRepositoryLocation("newlocation"); return cruiseConfig; } }); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(goConfigService.getCurrentConfig().server().getCommandRepositoryLocation(), is("newlocation")); } @Test public void shouldRemoveCorrespondingRemotePipelinesFromCachedGoConfigIfTheConfigRepoIsDeleted() { final ConfigRepoConfig repoConfig1 = new ConfigRepoConfig(MaterialConfigsMother.gitMaterialConfig("url1"), XmlPartialConfigProvider.providerName); final ConfigRepoConfig repoConfig2 = new ConfigRepoConfig(MaterialConfigsMother.gitMaterialConfig("url2"), XmlPartialConfigProvider.providerName); goConfigService.updateConfig(new UpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getConfigRepos().add(repoConfig1); cruiseConfig.getConfigRepos().add(repoConfig2); return cruiseConfig; } }); PartialConfig partialConfigInRepo1 = PartialConfigMother.withPipeline("pipeline_in_repo1", new RepoConfigOrigin(repoConfig1, "repo1_r1")); PartialConfig partialConfigInRepo2 = PartialConfigMother.withPipeline("pipeline_in_repo2", new RepoConfigOrigin(repoConfig2, "repo2_r1")); goPartialConfig.onSuccessPartialConfig(repoConfig1, partialConfigInRepo1); goPartialConfig.onSuccessPartialConfig(repoConfig2, partialConfigInRepo2); // introduce an invalid change in repo1 so that there is a server health message corresponding to it PartialConfig invalidPartialInRepo1Revision2 = PartialConfigMother.invalidPartial("pipeline_in_repo1", new RepoConfigOrigin(repoConfig1, "repo1_r2")); goPartialConfig.onSuccessPartialConfig(repoConfig1, invalidPartialInRepo1Revision2); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig1)).size(), is(1)); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig1)).get(0).getMessage(), is("Invalid Merged Configuration")); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig1)).get(0).getDescription(), is("1+ errors :: Invalid stage name ''. This must be alphanumeric and can contain underscores and periods (however, it cannot start with a period). The maximum allowed length is 255 characters.;; - Config-Repo: url1 at repo1_r2")); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig2)).isEmpty(), is(true)); int countBeforeDeletion = cachedGoConfig.currentConfig().getConfigRepos().size(); ConfigSaveState configSaveState = cachedGoConfig.writeWithLock(new UpdateConfigCommand() { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getConfigRepos().remove(repoConfig1); return cruiseConfig; } }); assertThat(configSaveState, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.currentConfig().getConfigRepos().size(), is(countBeforeDeletion - 1)); assertThat(cachedGoConfig.currentConfig().getConfigRepos().contains(repoConfig2), is(true)); assertThat(cachedGoConfig.currentConfig().getAllPipelineNames().contains(new CaseInsensitiveString("pipeline_in_repo1")), is(false)); assertThat(cachedGoConfig.currentConfig().getAllPipelineNames().contains(new CaseInsensitiveString("pipeline_in_repo2")), is(true)); assertThat(cachedGoPartials.lastKnownPartials().size(), is(1)); assertThat(((RepoConfigOrigin) cachedGoPartials.lastKnownPartials().get(0).getOrigin()).getMaterial().getFingerprint().equals(repoConfig2.getMaterialConfig().getFingerprint()), is(true)); assertThat(ListUtil.find(cachedGoPartials.lastKnownPartials(), new ListUtil.Condition() { @Override public <T> boolean isMet(T item) { PartialConfig partialConfig = (PartialConfig) item; return ((RepoConfigOrigin) partialConfig.getOrigin()).getMaterial().getFingerprint().equals(repoConfig1.getMaterialConfig().getFingerprint()); } }), is(nullValue())); assertThat(cachedGoPartials.lastValidPartials().size(), is(1)); assertThat(((RepoConfigOrigin) cachedGoPartials.lastValidPartials().get(0).getOrigin()).getMaterial().getFingerprint().equals(repoConfig2.getMaterialConfig().getFingerprint()), is(true)); assertThat(ListUtil.find(cachedGoPartials.lastValidPartials(), new ListUtil.Condition() { @Override public <T> boolean isMet(T item) { PartialConfig partialConfig = (PartialConfig) item; return ((RepoConfigOrigin) partialConfig.getOrigin()).getMaterial().getFingerprint().equals(repoConfig1.getMaterialConfig().getFingerprint()); } }), is(nullValue())); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig1)).isEmpty(), is(true)); assertThat(serverHealthService.filterByScope(HealthStateScope.forPartialConfigRepo(repoConfig2)).isEmpty(), is(true)); } @Test public void shouldUpdateConfigWhenPartialsAreNotConfigured() throws GitAPIException, IOException { String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); BasicCruiseConfig config = GoConfigMother.configWithPipelines("pipeline1"); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(config, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.loadForEditing(), is(config)); assertNotEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); } @Test public void writeFullConfigWithLockShouldUpdateReloadStrategyToEnsureReloadIsSkippedInAbsenceOfConfigFileChanges() throws GitAPIException, IOException { BasicCruiseConfig config = GoConfigMother.configWithPipelines("pipeline1"); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(config, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); assertThat(state, is(ConfigSaveState.UPDATED)); cachedGoConfig.forceReload(); String gitShaAfterReload = configRepository.getCurrentRevCommit().getName(); assertThat(gitShaAfterReload, is(gitShaAfterSave)); } @Test public void shouldUpdateConfigWhenPartialsAreConfigured() throws GitAPIException, IOException { String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); PartialConfig validPartial = PartialConfigMother.withPipeline("remote_pipeline", new RepoConfigOrigin(configRepo, "revision1")); goPartialConfig.onSuccessPartialConfig(configRepo, validPartial); assertThat(cachedGoPartials.lastValidPartials().contains(validPartial), is(true)); assertThat(cachedGoPartials.lastKnownPartials().contains(validPartial), is(true)); CruiseConfig config = new Cloner().deepClone(cachedGoConfig.loadForEditing()); config.addEnvironment(UUID.randomUUID().toString()); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(config, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.loadForEditing(), is(config)); assertNotEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); assertThat(cachedGoPartials.lastValidPartials().contains(validPartial), is(true)); assertThat(cachedGoPartials.lastKnownPartials().contains(validPartial), is(true)); } @Test public void shouldUpdateConfigWithNoValidPartialsAndInvalidKnownPartials() throws GitAPIException, IOException { String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); PartialConfig invalidPartial = PartialConfigMother.invalidPartial("invalid", new RepoConfigOrigin(configRepo, "revision1")); goPartialConfig.onSuccessPartialConfig(configRepo, invalidPartial); assertTrue(cachedGoPartials.lastValidPartials().isEmpty()); assertTrue(cachedGoPartials.lastKnownPartials().contains(invalidPartial)); CruiseConfig config = new Cloner().deepClone(cachedGoConfig.loadForEditing()); config.addEnvironment(UUID.randomUUID().toString()); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(config, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.loadForEditing(), is(config)); assertNotEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); assertTrue(cachedGoPartials.lastValidPartials().isEmpty()); assertTrue(cachedGoPartials.lastKnownPartials().contains(invalidPartial)); } @Test public void shouldUpdateConfigWithValidPartialsAndInvalidKnownPartials() throws GitAPIException, IOException { String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); PartialConfig validPartial = PartialConfigMother.withPipeline("remote_pipeline", new RepoConfigOrigin(configRepo, "revision1")); PartialConfig invalidPartial = PartialConfigMother.invalidPartial("invalid", new RepoConfigOrigin(configRepo, "revision2")); goPartialConfig.onSuccessPartialConfig(configRepo, validPartial); goPartialConfig.onSuccessPartialConfig(configRepo, invalidPartial); assertTrue(cachedGoPartials.lastValidPartials().contains(validPartial)); assertTrue(cachedGoPartials.lastKnownPartials().contains(invalidPartial)); CruiseConfig config = new Cloner().deepClone(cachedGoConfig.loadForEditing()); config.addEnvironment(UUID.randomUUID().toString()); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(config, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.loadForEditing(), is(config)); assertNotEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); assertTrue(cachedGoPartials.lastValidPartials().contains(validPartial)); assertTrue(cachedGoPartials.lastKnownPartials().contains(invalidPartial)); } @Test public void shouldErrorOutOnUpdateConfigWithValidPartials_WithMainConfigBreakingPartials() throws GitAPIException, IOException { setupExternalConfigRepoWithDependencyMaterialOnPipelineInMainXml("upstream", "downstream"); String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); CruiseConfig originalConfig = cachedGoConfig.loadForEditing(); CruiseConfig editedConfig = new Cloner().deepClone(originalConfig); editedConfig.getGroups().remove(editedConfig.findGroup("default")); try { cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(editedConfig, goConfigService.configFileMd5())); fail("Expected the test to fail"); } catch (Exception e) { String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(cachedGoConfig.loadForEditing(), is(originalConfig)); assertEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); RepoConfigOrigin origin = (RepoConfigOrigin) cachedGoPartials.lastValidPartials().get(0).getOrigin(); assertThat(origin.getRevision(), is("r1")); } } @Test public void shouldMarkAPreviousInvalidPartialAsValid_IfMainXMLSatisfiesTheDependency() throws GitAPIException, IOException { String gitShaBeforeSave = configRepository.getCurrentRevCommit().getName(); PipelineConfig upstream = PipelineConfigMother.createPipelineConfig("upstream", "S", "J"); PartialConfig partialConfig = PartialConfigMother.pipelineWithDependencyMaterial("downstream", upstream, new RepoConfigOrigin(configRepo, "r2")); goPartialConfig.onSuccessPartialConfig(configRepo, partialConfig); assertTrue(cachedGoPartials.lastKnownPartials().contains(partialConfig)); assertTrue(cachedGoPartials.lastValidPartials().isEmpty()); CruiseConfig originalConfig = cachedGoConfig.loadForEditing(); CruiseConfig editedConfig = new Cloner().deepClone(originalConfig); editedConfig.addPipeline("default", upstream); ConfigSaveState state = cachedGoConfig.writeFullConfigWithLock(new FullConfigUpdateCommand(editedConfig, goConfigService.configFileMd5())); String gitShaAfterSave = configRepository.getCurrentRevCommit().getName(); String configXmlFromConfigFolder = FileUtil.readContentFromFile(new File(goConfigDao.fileLocation())); assertThat(state, is(ConfigSaveState.UPDATED)); assertThat(cachedGoConfig.loadForEditing(), is(editedConfig)); assertNotEquals(gitShaBeforeSave, gitShaAfterSave); assertThat(cachedGoConfig.loadForEditing().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(cachedGoConfig.currentConfig().getMd5(), is(configRepository.getCurrentRevision().getMd5())); assertThat(configXmlFromConfigFolder, is(configRepository.getCurrentRevision().getContent())); RepoConfigOrigin origin = (RepoConfigOrigin) cachedGoPartials.lastValidPartials().get(0).getOrigin(); assertThat(origin.getRevision(), is("r2")); assertTrue(cachedGoPartials.lastKnownPartials().contains(partialConfig)); assertTrue(cachedGoPartials.lastValidPartials().contains(partialConfig)); } private void addPipelineWithParams(CruiseConfig cruiseConfig) { PipelineConfig pipelineConfig = PipelineConfigMother.createPipelineConfig("mingle", "dev", "ant"); pipelineConfig.addParam(new ParamConfig("foo", "hg-server")); pipelineConfig.addParam(new ParamConfig("bar", "repo-name")); pipelineConfig.addMaterialConfig(MaterialConfigsMother.hgMaterialConfig("http://#{foo}/#{bar}", "folder")); cruiseConfig.addPipeline("another", pipelineConfig); } public MaterialConfig byFolder(MaterialConfigs materialConfigs, String folder) { for (MaterialConfig materialConfig : materialConfigs) { if (materialConfig instanceof ScmMaterialConfig && ObjectUtil.nullSafeEquals(folder, materialConfig.getFolder())) { return materialConfig; } } return null; } private UpdateConfigCommand updateFirstAgentResources(final String resources) { return new UpdateConfigCommand() { public CruiseConfig update(CruiseConfig cruiseConfig) { AgentConfig agentConfig = cruiseConfig.agents().get(0); agentConfig.setResources(new Resources(resources)); return cruiseConfig; } }; } private void deletePartial(String partial) { FileUtils.deleteQuietly(new File(externalConfigRepo, partial)); gitAddDotAndCommit(externalConfigRepo); } private void checkinPartial(String partial) throws IOException { File externalConfigRepo = this.externalConfigRepo; checkInPartial(partial, externalConfigRepo); } private void checkInPartial(String partial, File externalConfigRepo) throws IOException { ClassPathResource resource = new ClassPathResource(partial); if (resource.getFile().isDirectory()) { FileUtils.copyDirectory(resource.getFile(), externalConfigRepo); } else { FileUtils.copyFileToDirectory(resource.getFile(), externalConfigRepo); } gitAddDotAndCommit(externalConfigRepo); } private class ConfigChangeListenerStub implements ConfigChangedListener { private int invocationCount = 0; @Override public void onConfigChange(CruiseConfig newCruiseConfig) { invocationCount++; } } private String setupExternalConfigRepo(File configRepo) throws IOException { String configRepoTestResource = "external_git_config_repo"; return setupExternalConfigRepo(configRepo, configRepoTestResource); } private String setupExternalConfigRepo(File configRepo, String configRepoTestResource) throws IOException { ClassPathResource resource = new ClassPathResource(configRepoTestResource); FileUtils.copyDirectory(resource.getFile(), configRepo); CommandLine.createCommandLine("git").withArg("init").withArg(configRepo.getAbsolutePath()).runOrBomb(""); gitAddDotAndCommit(configRepo); ConsoleResult consoleResult = CommandLine.createCommandLine("git").withArg("log").withArg("-1").withArg("--pretty=format:%h").withWorkingDir(configRepo).runOrBomb(""); return consoleResult.outputAsString(); } private void gitAddDotAndCommit(File configRepo) { CommandLine.createCommandLine("git").withArg("add").withArg("-A").withArg(".").withWorkingDir(configRepo).runOrBomb(""); CommandLine.createCommandLine("git").withArg("config").withArg("user.email").withArg("go_test@go_test.me").withWorkingDir(configRepo).runOrBomb(""); CommandLine.createCommandLine("git").withArg("config").withArg("user.name").withArg("user").withWorkingDir(configRepo).runOrBomb(""); CommandLine.createCommandLine("git").withArg("commit").withArg("-m").withArg("initial commit").withWorkingDir(configRepo).runOrBomb(""); } }