diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /configserver/src/test/java/com/yahoo |
Publish
Diffstat (limited to 'configserver/src/test/java/com/yahoo')
144 files changed, 11322 insertions, 0 deletions
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java new file mode 100644 index 00000000000..e5d98bda7b5 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationMapperTest.java @@ -0,0 +1,66 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.http.NotFoundException; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ApplicationMapperTest { + ApplicationId appId; + ApplicationMapper applicationMapper; + ArrayList<Version> vespaVersions = new ArrayList<>(); + ArrayList<Application> applications = new ArrayList<>(); + + @Before + public void setUp() { + applicationMapper = new ApplicationMapper(); + appId = new ApplicationId.Builder() + .tenant("test").applicationName("test").instanceName("test").build(); + vespaVersions.add(Version.fromString("1.2.3")); + vespaVersions.add(Version.fromString("1.2.4")); + vespaVersions.add(Version.fromString("1.2.5")); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(0), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(1), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(2), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + } + + @Test + public void testGetForVersionReturnsCorrectVersion() { + applicationMapper.register(appId, ApplicationSet.fromList(applications)); + assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(0))), applications.get(0)); + assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(1))), applications.get(1)); + assertEquals(applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(2))), applications.get(2)); + } + + @Test + public void testGetForVersionReturnsLatestVersion() { + applicationMapper.register(appId, ApplicationSet.fromList(applications)); + assertEquals(applicationMapper.getForVersion(appId, Optional.empty()), applications.get(2)); + } + + @Test (expected = VersionDoesNotExistException.class) + public void testGetForVersionThrows() { + applicationMapper.register(appId, ApplicationSet.fromList(Arrays.asList(applications.get(0), applications.get(2)))); + + applicationMapper.getForVersion(appId, Optional.of(vespaVersions.get(1))); + } + + @Test (expected = NotFoundException.class) + public void testGetForVersionThrows2() { + applicationMapper.register(appId, ApplicationSet.fromSingle(applications.get(0))); + + applicationMapper.getForVersion(new ApplicationId.Builder() + .tenant("different").applicationName("different").instanceName("different").build(), + Optional.of(vespaVersions.get(1))); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java new file mode 100644 index 00000000000..fce6139e18e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationSetTest.java @@ -0,0 +1,56 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * @author Vegard Sjonfjell + */ +public class ApplicationSetTest { + + ApplicationSet applicationSet; + List<Version> vespaVersions = new ArrayList<>(); + List<Application> applications = new ArrayList<>(); + + @Before + public void setUp() { + vespaVersions.add(Version.fromString("1.2.3")); + vespaVersions.add(Version.fromString("1.2.4")); + vespaVersions.add(Version.fromString("1.2.5")); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(0), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(1), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + applications.add(new Application(new ModelStub(), null, 0, vespaVersions.get(2), MetricUpdater.createTestUpdater(), ApplicationId.defaultId())); + } + + @Test + public void testGetForVersionOrLatestReturnsCorrectVersion() { + applicationSet = ApplicationSet.fromList(applications); + assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(0))), applications.get(0)); + assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(1))), applications.get(1)); + assertEquals(applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(2))), applications.get(2)); + } + + @Test + public void testGetForVersionOrLatestReturnsLatestVersion() { + applicationSet = ApplicationSet.fromList(applications); + assertEquals(applicationSet.getForVersionOrLatest(Optional.empty()), applications.get(2)); + } + + @Test (expected = VersionDoesNotExistException.class) + public void testGetForVersionOrLatestThrows() { + applicationSet = ApplicationSet.fromList(Arrays.asList(applications.get(0), applications.get(2))); + applicationSet.getForVersionOrLatest(Optional.of(vespaVersions.get(1))); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java new file mode 100644 index 00000000000..5dd0f889431 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/CompressedApplicationInputStreamTest.java @@ -0,0 +1,172 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.io.ByteStreams; +import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.Test; + +import java.io.*; +import java.util.Arrays; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author lulf + * @since 5.1 + */ +public class CompressedApplicationInputStreamTest { + + static void writeFileToTar(ArchiveOutputStream taos, File file) throws IOException { + taos.putArchiveEntry(taos.createArchiveEntry(file, file.getName())); + ByteStreams.copy(new FileInputStream(file), taos); + taos.closeArchiveEntry(); + } + + public static File createArchiveFile(ArchiveOutputStream taos, File outFile) throws IOException { + File app = new File("src/test/resources/deploy/validapp"); + writeFileToTar(taos, new File(app, "services.xml")); + writeFileToTar(taos, new File(app, "hosts.xml")); + taos.close(); + return outFile; + } + + public static File createTarFile() throws IOException { + File outFile = File.createTempFile("testapp", ".tar.gz"); + ArchiveOutputStream archiveOutputStream = new TarArchiveOutputStream(new GZIPOutputStream(new FileOutputStream(outFile))); + return createArchiveFile(archiveOutputStream, outFile); + } + + public static File createZipFile() throws IOException { + File outFile = File.createTempFile("testapp", ".tar.gz"); + ArchiveOutputStream archiveOutputStream = new ZipArchiveOutputStream(new FileOutputStream(outFile)); + return createArchiveFile(archiveOutputStream, outFile); + } + + void assertTestApp(File outApp) { + String [] files = outApp.list(); + assertThat(files.length, is(2)); + if ("hosts.xml".equals(files[0])) { + assertThat(files[1], is("services.xml")); + } else if ("hosts.xml".equals(files[1])) { + assertThat(files[0], is("services.xml")); + } else { + fail("Both services.xml and hosts.xml should be contained in the unpacked application"); + } + } + + @Test + public void require_that_valid_tar_application_can_be_unpacked() throws IOException { + File outFile = createTarFile(); + CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(outFile)))); + File outApp = unpacked.decompress(); + assertTestApp(outApp); + } + + @Test + public void require_that_valid_tar_application_in_subdir_can_be_unpacked() throws IOException { + File outFile = File.createTempFile("testapp", ".tar.gz"); + ArchiveOutputStream archiveOutputStream = new TarArchiveOutputStream(new GZIPOutputStream(new FileOutputStream(outFile))); + + File app = new File("src/test/resources/deploy/validapp"); + + File file = new File(app, "services.xml"); + archiveOutputStream.putArchiveEntry(archiveOutputStream.createArchiveEntry(file, "application/" + file.getName())); + ByteStreams.copy(new FileInputStream(file), archiveOutputStream); + archiveOutputStream.closeArchiveEntry(); + file = new File(app, "hosts.xml"); + archiveOutputStream.putArchiveEntry(archiveOutputStream.createArchiveEntry(file, "application/" + file.getName())); + ByteStreams.copy(new FileInputStream(file), archiveOutputStream); + archiveOutputStream.closeArchiveEntry(); + + archiveOutputStream.close(); + + CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream(new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(outFile)))); + File outApp = unpacked.decompress(); + assertThat(outApp.getName(), is("application")); // gets the name of the subdir + assertTestApp(outApp); + } + + @Test + public void require_that_valid_zip_application_can_be_unpacked() throws IOException { + File outFile = createZipFile(); + CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream( + new ZipArchiveInputStream(new FileInputStream(outFile))); + File outApp = unpacked.decompress(); + assertTestApp(outApp); + } + + @Test + public void require_that_gnu_tared_file_can_be_unpacked() throws IOException, InterruptedException { + File tmpTar = File.createTempFile("myapp", ".tar"); + Process p = new ProcessBuilder("tar", "-C", "src/test/resources/deploy/validapp", "--exclude=.svn", "-cvf", tmpTar.getAbsolutePath(), ".").start(); + p.waitFor(); + p = new ProcessBuilder("gzip", tmpTar.getAbsolutePath()).start(); + p.waitFor(); + File gzFile = new File(tmpTar.getAbsolutePath() + ".gz"); + assertTrue(gzFile.exists()); + CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream( + new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(gzFile)))); + File outApp = unpacked.decompress(); + assertTestApp(outApp); + } + + @Test + public void require_that_nested_app_can_be_unpacked() throws IOException, InterruptedException { + File tmpTar = File.createTempFile("myapp", ".tar"); + Process p = new ProcessBuilder("tar", "-C", "src/test/resources/deploy/advancedapp", "--exclude=.svn", "-cvf", tmpTar.getAbsolutePath(), ".").start(); + p.waitFor(); + p = new ProcessBuilder("gzip", tmpTar.getAbsolutePath()).start(); + p.waitFor(); + File gzFile = new File(tmpTar.getAbsolutePath() + ".gz"); + assertTrue(gzFile.exists()); + CompressedApplicationInputStream unpacked = CompressedApplicationInputStream.createFromCompressedStream( + new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(gzFile)))); + File outApp = unpacked.decompress(); + List<File> files = Arrays.asList(outApp.listFiles()); + assertThat(files.size(), is(4)); + assertTrue(files.contains(new File(outApp, "services.xml"))); + assertTrue(files.contains(new File(outApp, "hosts.xml"))); + assertTrue(files.contains(new File(outApp, "searchdefinitions"))); + assertTrue(files.contains(new File(outApp, "external"))); + File sd = files.get(files.indexOf(new File(outApp, "searchdefinitions"))); + assertTrue(sd.isDirectory()); + assertThat(sd.listFiles().length, is(1)); + assertThat(sd.listFiles()[0].getAbsolutePath(), is(new File(sd, "keyvalue.sd").getAbsolutePath())); + + File ext = files.get(files.indexOf(new File(outApp, "external"))); + assertTrue(ext.isDirectory()); + assertThat(ext.listFiles().length, is(1)); + assertThat(ext.listFiles()[0].getAbsolutePath(), is(new File(ext, "foo").getAbsolutePath())); + + files = Arrays.asList(ext.listFiles()); + File foo = files.get(files.indexOf(new File(ext, "foo"))); + assertTrue(foo.isDirectory()); + assertThat(foo.listFiles().length, is(1)); + assertThat(foo.listFiles()[0].getAbsolutePath(), is(new File(foo, "bar").getAbsolutePath())); + + files = Arrays.asList(foo.listFiles()); + File bar = files.get(files.indexOf(new File(foo, "bar"))); + assertTrue(bar.isDirectory()); + assertThat(bar.listFiles().length, is(1)); + assertTrue(bar.listFiles()[0].isFile()); + assertThat(bar.listFiles()[0].getAbsolutePath(), is(new File(bar, "lol").getAbsolutePath())); + } + + + @Test(expected = IOException.class) + public void require_that_invalid_application_returns_error_when_unpacked() throws IOException { + File app = new File("src/test/resources/deploy/validapp/services.xml"); + CompressedApplicationInputStream.createFromCompressedStream( + new TarArchiveInputStream(new GZIPInputStream(new FileInputStream(app)))); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java new file mode 100644 index 00000000000..f127d8716aa --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigResponseFactoryTest.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import org.junit.Before; +import org.junit.Test; + +import java.io.StringReader; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.19 + */ +public class ConfigResponseFactoryTest { + private InnerCNode def; + + + @Before + public void setup() { + DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n"))); + def = dParser.getTree(); + } + + @Test + public void testUncompressedFacory() { + UncompressedConfigResponseFactory responseFactory = new UncompressedConfigResponseFactory(); + ConfigResponse response = responseFactory.createResponse(ConfigPayload.empty(), def, 3); + assertThat(response.getCompressionInfo().getCompressionType(), is(CompressionType.UNCOMPRESSED)); + assertThat(response.getGeneration(), is(3l)); + assertThat(response.getPayload().getByteLength(), is(2)); + } + + @Test + public void testLZ4CompressedFacory() { + LZ4ConfigResponseFactory responseFactory = new LZ4ConfigResponseFactory(); + ConfigResponse response = responseFactory.createResponse(ConfigPayload.empty(), def, 3); + assertThat(response.getCompressionInfo().getCompressionType(), is(CompressionType.LZ4)); + assertThat(response.getGeneration(), is(3l)); + assertThat(response.getPayload().getByteLength(), is(3)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java new file mode 100644 index 00000000000..6f5b51e7914 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.TenantName; +import com.yahoo.io.IOUtils; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.version.VersionState; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class ConfigServerBootstrapTest extends TestWithTenant { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testConfigServerBootstrap() throws Exception { + File versionFile = temporaryFolder.newFile(); + ConfigserverConfig.Builder config = new ConfigserverConfig.Builder(); + MockTenantRequestHandler myServer = new MockTenantRequestHandler(Metrics.createTestMetrics()); + MockRpc rpc = new MockRpc(new ConfigserverConfig(config).rpcport()); + + assertFalse(myServer.started); + assertFalse(myServer.stopped); + VersionState versionState = new VersionState(versionFile); + assertTrue(versionState.isUpgraded()); + ConfigServerBootstrap bootstrap = new ConfigServerBootstrap(tenants, rpc, (application, timeout) -> Optional.empty(), versionState); + assertFalse(versionState.isUpgraded()); + assertThat(versionState.currentVersion(), is(versionState.storedVersion())); + assertThat(IOUtils.readAll(new FileReader(versionFile)), is(versionState.currentVersion().toSerializedForm())); + waitUntilStarted(rpc, 60000); + assertTrue(rpc.started); + assertFalse(rpc.stopped); + bootstrap.deconstruct(); + assertTrue(rpc.started); + assertTrue(rpc.stopped); + } + + private void waitUntilStarted(MockRpc server, long timeout) throws InterruptedException { + long start = System.currentTimeMillis(); + while ((System.currentTimeMillis() - start) < timeout) { + if (server.started) + return; + Thread.sleep(10); + } + } + + public static class MockTenantRequestHandler extends TenantRequestHandler { + public volatile boolean started = false; + public volatile boolean stopped = false; + + public MockTenantRequestHandler(Metrics statistics) { + super(statistics, TenantName.from("testTenant"), new ArrayList<>(), new UncompressedConfigResponseFactory(), new HostRegistries()); + } + } + + public static class MockRpc extends com.yahoo.vespa.config.server.MockRpc { + public volatile boolean started = false; + public volatile boolean stopped = false; + + public MockRpc(int port) { + super(port); + } + + @Override + public void run() { + started = true; + } + + @Override + public void stop() { + stopped = true; + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java new file mode 100644 index 00000000000..0b07fd5b8e7 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerDBTest.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.io.Files; +import com.yahoo.io.IOUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class ConfigServerDBTest { + private ConfigServerDB serverDB; + private File tempDir; + + @Before + public void setup() { + tempDir = Files.createTempDir(); + serverDB = ConfigServerDB.createTestConfigServerDb(tempDir.getAbsolutePath()); + } + + private ConfigServerDB createInitializer(File pluginDir) throws IOException { + File existingDef = new File(serverDB.classes(), "test.def"); + IOUtils.writeFile(existingDef, "hello", false); + return ConfigServerDB.createTestConfigServerDb(tempDir.getAbsolutePath()); + } + + @Test + public void require_that_existing_def_files_are_copied() throws IOException { + assertThat(serverDB.serverdefs().listFiles().length, is(0)); + createInitializer(Files.createTempDir()); + assertThat(serverDB.serverdefs().listFiles().length, is(1)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java new file mode 100644 index 00000000000..7f9ac50c549 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/DelayedConfigResponseTest.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; + +import com.yahoo.jrt.Request; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; +import com.yahoo.vespa.config.protocol.Trace; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class DelayedConfigResponseTest { + + @Test + public void testDelayedConfigResponses() { + + MockRpc rpc = new MockRpc(13337); + DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false); + assertThat(responses.size(), is(0)); + JRTServerConfigRequest req = createRequest("foo", "md5", "myid", "mymd5", 3, 1000000, "bar"); + req.setDelayedResponse(true); + GetConfigContext context = GetConfigContext.testContext(ApplicationId.defaultId()); + responses.delayResponse(req, context); + assertThat(responses.size(), is(0)); + + req.setDelayedResponse(false); + responses.delayResponse(req, context); + responses.delayResponse(createRequest("foolio", "md5", "myid", "mymd5", 3, 100000, "bar"), context); + assertThat(responses.size(), is(2)); + assertTrue(req.isDelayedResponse()); + List<DelayedConfigResponses.DelayedConfigResponse> it = responses.allDelayedResponses(); + assertTrue(!it.isEmpty()); + } + + @Test + public void testDelayResponseRemove() { + GetConfigContext context = GetConfigContext.testContext(ApplicationId.defaultId()); + MockRpc rpc = new MockRpc(13337); + DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false); + responses.delayResponse(createRequest("foolio", "md5", "myid", "mymd5", 3, 100000, "bar"), context); + assertThat(responses.size(), is(1)); + responses.allDelayedResponses().get(0).cancel(); + assertThat(responses.size(), is(0)); + } + + @Test + public void testDelayedConfigResponse() { + MockRpc rpc = new MockRpc(13337); + DelayedConfigResponses responses = new DelayedConfigResponses(rpc, 1, false); + assertThat(responses.size(), is(0)); + assertThat(responses.toString(), is("DelayedConfigResponses. Average Size=0")); + JRTServerConfigRequest req = createRequest("foo", "md5", "myid", "mymd5", 3, 100, "bar"); + responses.delayResponse(req, GetConfigContext.testContext(ApplicationId.defaultId())); + rpc.waitUntilSet(5000); + assertThat(rpc.latestRequest, is(req)); + } + + public JRTServerConfigRequest createRequest(String configName, String defMd5, String configId, String md5, long generation, long timeout, String namespace) { + Request request = JRTClientConfigRequestV3. + createWithParams(new ConfigKey<>(configName, configId, namespace, defMd5, null), DefContent.fromList(Collections.emptyList()), + "fromHost", md5, generation, timeout, Trace.createDummy(), CompressionType.UNCOMPRESSED, + Optional.empty()).getRequest(); + return JRTServerConfigRequestV3.createFromRequest(request); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java new file mode 100644 index 00000000000..b34881bcba8 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/DeployHandlerLoggerTest.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class DeployHandlerLoggerTest { + @Test + public void test_verbose_logging() throws IOException { + testLogging(true, ".*time.*level\":\"DEBUG\".*message.*time.*level\":\"SPAM\".*message.*time.*level\":\"FINE\".*message.*time.*level\":\"WARNING\".*message.*"); + } + + @Test + public void test_normal_logging() throws IOException { + testLogging(false, ".*\\{\"time.*level\":\"WARNING\".*message.*"); + } + + private void testLogging(boolean verbose, String expectedPattern) throws IOException { + Slime slime = new Slime(); + Cursor array = slime.setArray(); + DeployLogger logger = new DeployHandlerLogger(array, verbose, new ApplicationId.Builder() + .tenant("testtenant").applicationName("testapp").build()); + logMessages(logger); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new JsonFormat(true).encode(baos, slime); + assertTrue(Pattern.matches(expectedPattern, baos.toString())); + } + + private void logMessages(DeployLogger logger) { + logger.log(LogLevel.DEBUG, "foobar"); + logger.log(LogLevel.SPAM, "foobar"); + logger.log(LogLevel.FINE, "baz"); + logger.log(LogLevel.WARNING, "baz"); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java new file mode 100644 index 00000000000..cd37b4a31c7 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/GetConfigProcessorTest.java @@ -0,0 +1,119 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.SentinelConfig; +import com.yahoo.config.provision.TenantName; +import com.yahoo.text.Utf8; +import com.yahoo.text.Utf8Array; +import com.yahoo.text.Utf8String; +import com.yahoo.vespa.config.ConfigKey; + +import com.yahoo.vespa.config.protocol.CompressionInfo; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; +import com.yahoo.vespa.config.protocol.Trace; +import org.junit.Test; +import static org.junit.Assert.assertFalse; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class GetConfigProcessorTest { + + @Test + public void testSentinelConfig() { + MockRpc rpc = new MockRpc(13337, false); + rpc.response = new MockConfigResponse("foo"); // should be a sentinel config, but it does not matter for this test + + // one tenant, which has host1 assigned + boolean pretentToHaveLoadedApplications = true; + TenantName testTenant = TenantName.from("test"); + rpc.onTenantCreate(testTenant, new MockTenantProvider(pretentToHaveLoadedApplications)); + rpc.hostsUpdated(testTenant, Collections.singleton("host1")); + + { // a config is returned normally + JRTServerConfigRequest req = createV3SentinelRequest("host1"); + GetConfigProcessor proc = new GetConfigProcessor(rpc, req, false); + proc.run(); + assertTrue(rpc.tryResolveConfig); + assertTrue(rpc.tryRespond); + assertThat(rpc.errorCode, is(0)); + } + + rpc.resetChecks(); + // host1 is replaced by host2 for this tenant + rpc.hostsUpdated(testTenant, Collections.singleton("host2")); + + { // this causes us to get an empty config instead of normal config resolution + JRTServerConfigRequest req = createV3SentinelRequest("host1"); + GetConfigProcessor proc = new GetConfigProcessor(rpc, req, false); + proc.run(); + assertFalse(rpc.tryResolveConfig); // <-- no normal config resolution happening + assertTrue(rpc.tryRespond); + assertThat(rpc.errorCode, is(0)); + } + } + + private static JRTServerConfigRequest createV3SentinelRequest(String fromHost) { + final ConfigKey<?> configKey = new ConfigKey<>(SentinelConfig.CONFIG_DEF_NAME, "myid", SentinelConfig.CONFIG_DEF_NAMESPACE); + return JRTServerConfigRequestV3.createFromRequest(JRTClientConfigRequestV3. + createWithParams(configKey, DefContent.fromList(Arrays.asList(SentinelConfig.CONFIG_DEF_SCHEMA)), + fromHost, "", 0, 100, Trace.createDummy(), CompressionType.UNCOMPRESSED, + Optional.empty()).getRequest()); + } + + private class MockConfigResponse implements ConfigResponse { + + private final String line; + public MockConfigResponse(String line) { + this.line = line; + } + + @Override + public Utf8Array getPayload() { + return new Utf8String(""); + } + + @Override + public List<String> getLegacyPayload() { + return Arrays.asList(line); + } + + @Override + public long getGeneration() { + return 1; + } + + @Override + public String getConfigMd5() { + return "mymd5"; + } + + @Override + public void serialize(OutputStream os, CompressionType uncompressed) throws IOException { + os.write(Utf8.toBytes(line)); + } + + @Override + public CompressionInfo getCompressionInfo() { + return CompressionInfo.uncompressed(); + } + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java new file mode 100644 index 00000000000..1147d2d4c0d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/HostRegistryTest.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Test; + +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.3 + */ +public class HostRegistryTest { + @Test + public void old_hosts_are_removed() { + HostRegistry<String> reg = new HostRegistry<>(); + assertNull(reg.getKeyForHost("foo.com")); + reg.update("fookey", Arrays.asList("foo.com", "bar.com", "baz.com")); + assertGetKey(reg, "foo.com", "fookey"); + assertGetKey(reg, "bar.com", "fookey"); + assertGetKey(reg, "baz.com", "fookey"); + assertThat(reg.getAllHosts().size(), is(3)); + reg.update("fookey", Arrays.asList("bar.com", "baz.com")); + assertNull(reg.getKeyForHost("foo.com")); + assertGetKey(reg, "bar.com", "fookey"); + assertGetKey(reg, "baz.com", "fookey"); + + assertThat(reg.getAllHosts().size(), is(2)); + assertThat(reg.getAllHosts(), contains("bar.com", "baz.com")); + reg.removeHostsForKey("fookey"); + assertThat(reg.getAllHosts().size(), is(0)); + assertNull(reg.getKeyForHost("foo.com")); + assertNull(reg.getKeyForHost("bar.com")); + } + + @Test + public void multiple_keys_are_handled() { + HostRegistry<String> reg = new HostRegistry<>(); + reg.update("fookey", Arrays.asList("foo.com", "bar.com")); + reg.update("barkey", Arrays.asList("baz.com", "quux.com")); + assertGetKey(reg, "foo.com", "fookey"); + assertGetKey(reg, "bar.com", "fookey"); + assertGetKey(reg, "baz.com", "barkey"); + assertGetKey(reg, "quux.com", "barkey"); + } + + @Test(expected = IllegalArgumentException.class) + public void keys_cannot_overlap() { + HostRegistry<String> reg = new HostRegistry<>(); + reg.update("fookey", Arrays.asList("foo.com", "bar.com")); + reg.update("barkey", Arrays.asList("bar.com", "baz.com")); + } + + @Test + public void all_hosts_are_returned() { + HostRegistry<String> reg = new HostRegistry<>(); + reg.update("fookey", Arrays.asList("foo.com", "bar.com")); + reg.update("barkey", Arrays.asList("baz.com", "quux.com")); + assertThat(reg.getAllHosts().size(), is(4)); + } + + @Test + public void ensure_that_collection_is_copied() { + HostRegistry<String> reg = new HostRegistry<>(); + List<String> hosts = new ArrayList<>(Arrays.asList("foo.com", "bar.com", "baz.com")); + reg.update("fookey", hosts); + assertThat(reg.getCurrentHosts("fookey").size(), is(3)); + hosts.remove(2); + assertThat(reg.getCurrentHosts("fookey").size(), is(3)); + } + + @Test + public void ensure_that_underlying_hosts_do_not_change() { + HostRegistry<String> reg = new HostRegistry<>(); + reg.update("fookey", new ArrayList<>(Arrays.asList("foo.com", "bar.com", "baz.com"))); + Collection<String> hosts = reg.getAllHosts(); + assertThat(hosts.size(), is(3)); + reg.update("fookey", new ArrayList<>(Arrays.asList("foo.com"))); + assertThat(hosts.size(), is(3)); + } + + private void assertGetKey(HostRegistry<String> reg, String host, String expectedKey) { + assertNotNull(reg.getKeyForHost(host)); + assertThat(reg.getKeyForHost(host), is(expectedKey)); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java new file mode 100644 index 00000000000..09576d18b32 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistryTest.java @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.io.Files; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.http.v2.SessionActiveHandlerTest; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.*; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.model.VespaModelFactory; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class InjectedGlobalComponentRegistryTest { + + private Curator curator; + private ConfigCurator configCurator; + private Metrics metrics; + private ConfigServerDB serverDB; + private SessionPreparer sessionPreparer; + private ConfigserverConfig configserverConfig; + private RpcServer rpcServer; + private SuperModelGenerationCounter generationCounter; + private ConfigDefinitionRepo defRepo; + private PermanentApplicationPackage permanentApplicationPackage; + private HostRegistries hostRegistries; + private GlobalComponentRegistry globalComponentRegistry; + private ModelFactoryRegistry modelFactoryRegistry; + private HostProvisionerProvider hostProvisionerProvider; + private Zone zone; + + @Before + public void setupRegistry() { + curator = new MockCurator(); + configCurator = ConfigCurator.create(curator); + metrics = Metrics.createTestMetrics(); + modelFactoryRegistry = new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))); + configserverConfig = new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(Files.createTempDir().getAbsolutePath())); + serverDB = new ConfigServerDB(configserverConfig); + sessionPreparer = new SessionTest.MockSessionPreparer(); + rpcServer = new RpcServer(configserverConfig, null, Metrics.createTestMetrics(), new HostRegistries()); + generationCounter = new SuperModelGenerationCounter(curator); + defRepo = new StaticConfigDefinitionRepo(); + permanentApplicationPackage = new PermanentApplicationPackage(configserverConfig); + hostRegistries = new HostRegistries(); + hostProvisionerProvider = HostProvisionerProvider.withProvisioner(new SessionActiveHandlerTest.MockProvisioner()); + zone = Zone.defaultZone(); + globalComponentRegistry = new InjectedGlobalComponentRegistry(curator, configCurator, metrics, modelFactoryRegistry, serverDB, sessionPreparer, rpcServer, configserverConfig, generationCounter, defRepo, permanentApplicationPackage, hostRegistries, hostProvisionerProvider, zone); + } + + @Test + public void testThatAllComponentsAreSetup() { + assertThat(globalComponentRegistry.getModelFactoryRegistry(), is(modelFactoryRegistry)); + assertThat(globalComponentRegistry.getServerDB(), is(serverDB)); + assertThat(globalComponentRegistry.getSessionPreparer(), is(sessionPreparer)); + assertThat(globalComponentRegistry.getMetrics(), is(metrics)); + assertThat(globalComponentRegistry.getCurator(), is(curator)); + assertThat(globalComponentRegistry.getConfigserverConfig(), is(configserverConfig)); + assertThat(globalComponentRegistry.getReloadListener().hashCode(), is(rpcServer.hashCode())); + assertThat(globalComponentRegistry.getTenantListener().hashCode(), is(rpcServer.hashCode())); + assertThat(globalComponentRegistry.getSuperModelGenerationCounter(), is(generationCounter)); + assertThat(globalComponentRegistry.getConfigDefinitionRepo(), is(defRepo)); + assertThat(globalComponentRegistry.getPermanentApplicationPackage(), is(permanentApplicationPackage)); + assertThat(globalComponentRegistry.getHostRegistries(), is(hostRegistries)); + assertThat(globalComponentRegistry.getZone(), is (zone)); + assertTrue(globalComponentRegistry.getHostProvisioner().isPresent()); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java new file mode 100644 index 00000000000..461da73638c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MemoryGenerationCounter.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.config.GenerationCounter; + +/** + * @author lulf + * @since 5. + */ +public class MemoryGenerationCounter implements GenerationCounter { + long value; + @Override + public long increment() { + return ++value; + } + + @Override + public long get() { + return value; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java new file mode 100644 index 00000000000..1ba660dc5d1 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MiscTestCase.java @@ -0,0 +1,56 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import static org.junit.Assert.*; +import java.io.*; +import java.util.List; +import java.util.ArrayList; +import org.junit.Test; +import com.yahoo.vespa.config.util.ConfigUtils; +import com.yahoo.config.AppConfig; +import com.yahoo.config.Md5testConfig; + +/** + * Tests that does not yet have a specific home due to removed classes, obsolete features etc. + * + * @author vegardh + */ +public class MiscTestCase { + + /** + * Verifies that the md5 sum computed on the server is equal to that in the generated class. + * + * @throws java.io.IOException if an error in zk + */ + @Test + public void testGetDefMd5() throws IOException { + System.out.println("\nStarting testGetDefMd5"); + final String defDir = "src/test/resources/configdefinitions/"; + assertEquals(AppConfig.CONFIG_DEF_MD5, ConfigUtils.getDefMd5(file2lines(new File(defDir + "app.def")))); + assertEquals(Md5testConfig.CONFIG_DEF_MD5, ConfigUtils.getDefMd5(file2lines(new File(defDir + "md5test.def")))); + } + + private static List<String> file2lines(File file) throws IOException { + List<String> lines = new ArrayList<>(); + LineNumberReader in = new LineNumberReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); + String line; + while ((line = in.readLine()) != null) { + lines.add(line); + } + return lines; + } + + @Test + public void testMd5StripSpaces() { + assertEquals("", ConfigUtils.stripSpaces("")); + assertEquals("foo", ConfigUtils.stripSpaces("foo")); + assertEquals(" foo", ConfigUtils.stripSpaces(" foo")); + assertEquals("bar ", ConfigUtils.stripSpaces("bar ")); + assertEquals("bar ", ConfigUtils.stripSpaces("bar ")); + assertEquals("b ar", ConfigUtils.stripSpaces("b \t ar")); + assertEquals("bar foo", ConfigUtils.stripSpaces("bar\t\tfoo")); + assertEquals("blabla string default=\"\t\"", ConfigUtils.stripSpaces("blabla string default=\"\t\"")); + assertEquals("blabla string default=\"foo\tbar\"", ConfigUtils.stripSpaces("blabla string default=\"foo\tbar\"")); + assertEquals("blabla string default=\" \t \"", ConfigUtils.stripSpaces("blabla string default=\" \t \"")); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java new file mode 100644 index 00000000000..d705203b5af --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockReloadHandler.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; + +/** + * @author lulf + * @since 5.1.24 + */ +public class MockReloadHandler implements ReloadHandler { + public ApplicationSet current = null; + public ReloadListener listener = null; + public volatile ApplicationId lastRemoved = null; + + @Override + public void reloadConfig(ApplicationSet application) { + this.current = application; + } + + @Override + public void removeApplication(ApplicationId applicationId) { + lastRemoved = applicationId; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java new file mode 100644 index 00000000000..373545f1a8b --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRequestHandler.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +import java.util.*; + +/** + * Test utility class + * @author lulf + * @since 5.25 + */ +public class MockRequestHandler implements RequestHandler, ReloadHandler, TenantHandlerProvider { + + volatile String serverStats = ""; + volatile boolean reloadResponse = false; + volatile boolean throwException = false; + public long appGeneration = 0; + private Set<ConfigKey<?>> allConfigs = new HashSet<>(); + public volatile ConfigResponse responseConfig = null; // for some v1 mocking + public Map<ApplicationId, ConfigResponse> responses = new LinkedHashMap<>(); // for v2 mocking + private final boolean pretendToHaveLoadedAnyApplication; + + public MockRequestHandler() { + this(false); + } + + public MockRequestHandler(boolean pretendToHaveLoadedAnyApplication) { + this.pretendToHaveLoadedAnyApplication = pretendToHaveLoadedAnyApplication; + } + + @Override + public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) { + if (appId==null) { + checkThrow(); + return responseConfig; + } + return responses.get(appId); + } + + @Override + public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) { + return Collections.emptySet(); + } + + @Override + public void removeApplication(ApplicationId applicationId) { + } + + @Override + public void reloadConfig(ApplicationSet application) { + checkThrow(); + } + + private void checkThrow() { + if (throwException) { + throw new RuntimeException("foo"); + } + } + + @Override + public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive) { + return Collections.emptySet(); + } + + @Override + public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion) { + Set<String> ret = new HashSet<>(); + for (ConfigKey<?> k : allConfigs) { + ret.add(k.getConfigId()); + } + return ret; + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) { + return allConfigs; + } + + public void setAllConfigs(Set<ConfigKey<?>> allConfigs) { + this.allConfigs = allConfigs; + } + + @Override + public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) { + if (pretendToHaveLoadedAnyApplication) return true; + return responses.containsKey(appId); + } + + @Override + public ApplicationId resolveApplicationId(String hostName) { + return ApplicationId.defaultId(); + } + + @Override + public RequestHandler getRequestHandler() { + return this; + } + + @Override + public ReloadHandler getReloadHandler() { + return this; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java new file mode 100644 index 00000000000..70e4b4f6bd0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockRpc.java @@ -0,0 +1,107 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; +import com.yahoo.vespa.config.server.monitoring.Metrics; + +import java.util.Optional; +import java.util.concurrent.CompletionService; + +/** + * Test utility mocking an RPC server. + * + * @author lulf + * @since 5.25 + */ +public class MockRpc extends RpcServer { + + public boolean forced = false; + public RuntimeException exception = null; + public int errorCode = 0; + public ConfigResponse response = null; + + // Fields used to assert on the calls made to this from tests + public boolean tryResolveConfig = false; + public boolean tryRespond = false; + /** The last request received and responded to */ + public volatile JRTServerConfigRequest latestRequest = null; + + + public MockRpc(int port, boolean createDefaultTenant, boolean pretendToHaveLoadedAnyApplication) { + super(createConfig(port), null, Metrics.createTestMetrics(), new HostRegistries()); + if (createDefaultTenant) { + onTenantCreate(TenantName.from("default"), new MockTenantProvider(pretendToHaveLoadedAnyApplication)); + } + } + + public MockRpc(int port, boolean createDefaultTenant) { + this(port, createDefaultTenant, true); + } + + public MockRpc(int port) { + this(port, true); + } + + /** Reset fields used to assert on the calls made to this */ + public void resetChecks() { + forced = false; + tryResolveConfig = false; + tryRespond = false; + latestRequest = null; + } + + private static ConfigserverConfig createConfig(int port) { + ConfigserverConfig.Builder b = new ConfigserverConfig.Builder(); + b.rpcport(port); + return new ConfigserverConfig(b); + } + + public boolean waitUntilSet(int timeout) { + long start = System.currentTimeMillis(); + long end = start + timeout; + while (start < end) { + if (latestRequest != null) + return true; + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return false; + } + + @Override + public Boolean addToRequestQueue(JRTServerConfigRequest request, boolean forceResponse, CompletionService<Boolean> completionService) { + latestRequest = request; + forced = forceResponse; + return true; + } + + @Override + public void respond(JRTServerConfigRequest request) { + latestRequest = request; + tryRespond = true; + errorCode = request.errorCode(); + } + + @Override + public ConfigResponse resolveConfig(JRTServerConfigRequest request, GetConfigContext context, Optional<Version> vespaVersion) { + tryResolveConfig = true; + if (exception != null) { + throw exception; + } + return response; + } + + @Override + public boolean isHostedVespa() { return true; } + + @Override + public boolean allTenantsLoaded() { return true; } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java new file mode 100644 index 00000000000..1fd92f2acc9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantListener.java @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.TenantName; + +/** + * @author lulf + * @since 5.8 + */ +public class MockTenantListener implements TenantListener { + TenantName tenantCreatedName; + TenantHandlerProvider provider; + TenantName tenantDeletedName; + boolean tenantsLoaded; + + @Override + public void onTenantCreate(TenantName tenantName, TenantHandlerProvider provider) { + this.tenantCreatedName = tenantName; + this.provider = provider; + } + + @Override + public void onTenantDelete(TenantName tenantName) { + this.tenantDeletedName = tenantName; + } + + @Override + public void onTenantsLoaded() { + tenantsLoaded = true; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java new file mode 100644 index 00000000000..4b92c91f581 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/MockTenantProvider.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +/** + * @author lulf + * @since 5. + */ +public class MockTenantProvider implements TenantHandlerProvider { + + final MockRequestHandler requestHandler; + final MockReloadHandler reloadHandler; + + public MockTenantProvider() { + this(false); + } + + public MockTenantProvider(boolean pretendToHaveLoadedAnyApplication) { + this.requestHandler = new MockRequestHandler(pretendToHaveLoadedAnyApplication); + this.reloadHandler = new MockReloadHandler(); + } + + @Override + public RequestHandler getRequestHandler() { return requestHandler; } + + @Override + public ReloadHandler getReloadHandler() { + return reloadHandler; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java new file mode 100644 index 00000000000..0352f2c9f3e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelContextImplTest.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.config.model.application.provider.MockFileRegistry; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.deploy.ModelContextImpl; + +import org.junit.Test; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + */ +public class ModelContextImplTest { + @Test + public void testModelContextTest() { + + final Rotation rotation = new Rotation("this.is.a.mock.rotation"); + final Set<Rotation> rotations = Collections.singleton(rotation); + + ModelContext context = new ModelContextImpl( + MockApplicationPackage.createEmpty(), + Optional.empty(), + Optional.empty(), + new BaseDeployLogger(), + new StaticConfigDefinitionRepo(), + new MockFileRegistry(), + Optional.empty(), + new ModelContextImpl.Properties( + ApplicationId.defaultId(), + true, + Collections.emptyList(), + false, + Zone.defaultZone(), + rotations), + Optional.empty(), + Optional.empty()); + assertTrue(context.applicationPackage() instanceof MockApplicationPackage); + assertFalse(context.hostProvisioner().isPresent()); + assertFalse(context.permanentApplicationPackage().isPresent()); + assertFalse(context.previousModel().isPresent()); + assertTrue(context.getFileRegistry() instanceof MockFileRegistry); + assertTrue(context.configDefinitionRepo() instanceof StaticConfigDefinitionRepo); + assertThat(context.properties().applicationId(), is(ApplicationId.defaultId())); + assertTrue(context.properties().configServerSpecs().isEmpty()); + assertTrue(context.properties().multitenant()); + assertTrue(context.properties().zone() instanceof Zone); + assertFalse(context.properties().hostedVespa()); + assertThat(context.properties().rotations(), equalTo(rotations)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java new file mode 100644 index 00000000000..3d0c50b9bcb --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelFactoryRegistryTest.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelCreateResult; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.http.UnknownVespaVersionException; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + */ +public class ModelFactoryRegistryTest { + @Test(expected = IllegalArgumentException.class) + public void testThatOneFactoryIsRequired() { + new ModelFactoryRegistry(new ComponentRegistry<>()); + } + + @Test + public void testThatLatestVersionIsSelected() { + Version versionA = Version.fromIntValues(5, 38, 4); + Version versionB = Version.fromIntValues(5, 58, 1); + Version versionC = Version.fromIntValues(5, 48, 44); + Version versionD = Version.fromIntValues(5, 18, 44); + TestFactory a = new TestFactory(versionA); + TestFactory b = new TestFactory(versionB); + TestFactory c = new TestFactory(versionC); + TestFactory d = new TestFactory(versionD); + + for (int i = 0; i < 100; i++) { + List<ModelFactory> randomOrder = Arrays.asList(a, b, c, d); + Collections.shuffle(randomOrder); + ModelFactoryRegistry registry = new ModelFactoryRegistry(randomOrder); + assertThat(registry.getFactory(versionA), is(a)); + assertThat(registry.getFactory(versionB), is(b)); + assertThat(registry.getFactory(versionC), is(c)); + assertThat(registry.getFactory(versionD), is(d)); + } + } + + @Test + public void testThatAllFactoriesAreReturned() { + TestFactory a = new TestFactory(Version.fromIntValues(5, 38, 4)); + TestFactory b = new TestFactory(Version.fromIntValues(5, 58, 1)); + TestFactory c = new TestFactory(Version.fromIntValues(5, 48, 44)); + TestFactory d = new TestFactory(Version.fromIntValues(5, 18, 44)); + ModelFactoryRegistry registry = new ModelFactoryRegistry(Arrays.asList(a, b, c, d)); + assertThat(registry.getFactories().size(), is(4)); + assertTrue(registry.getFactories().contains(a)); + assertTrue(registry.getFactories().contains(b)); + assertTrue(registry.getFactories().contains(c)); + assertTrue(registry.getFactories().contains(d)); + } + + @Test(expected = UnknownVespaVersionException.class) + public void testThatUnknownVersionGivesError() { + ModelFactoryRegistry registry = new ModelFactoryRegistry(Arrays.asList(new TestFactory(Version.fromIntValues(1, 2, 3)))); + registry.getFactory(Version.fromIntValues(3, 2, 1)); + } + + private static class TestFactory implements ModelFactory { + private final Version version; + + public TestFactory(Version version) { + this.version = version; + } + + @Override + public Version getVersion() { + return version; + } + + @Override + public Model createModel(ModelContext modelContext) { + return null; + } + + @Override + public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) { + return null; + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java new file mode 100644 index 00000000000..2615622fce0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ModelStub.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; + +import java.io.IOException; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +/** + * @author bratseth + */ +public class ModelStub implements Model { + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition targetDef, ConfigPayload override) throws IOException { + return null; + } + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, InnerCNode targetDef, ConfigPayload override) throws IOException { + return null; + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced() { + return null; + } + + @Override + public Collection<HostInfo> getHosts() { + return null; + } + + @Override + public Set<String> allConfigIds() { + return null; + } + + @Override + public void distributeFiles(FileDistribution fileDistribution) { + + } + + @Override + public Optional<ProvisionInfo> getProvisionInfo() { + return null; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java b/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java new file mode 100644 index 00000000000..0c76a907d82 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/PortRangeAllocator.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.Range; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; + +/** + * Allocates port ranges for all configserver tests. + * + * @author lulf + * @since 5.1.26 + */ +public class PortRangeAllocator { + private final static PortRange portRange = new PortRange(); + + // Get the next port from a pre-allocated range + public static int findAvailablePort() throws InterruptedException { + return portRange.next(); + } + + public static void releasePort(int port) { + portRange.release(port); + } + + private static class PortRange { + private final Set<Integer> takenPorts = new HashSet<>(); + private final Stack<Integer> freePorts = new Stack<>(); + private static final int first = 18651; + private static final int last = 18899; // see: factory/doc/port-ranges + + public PortRange() { + freePorts.addAll(ContiguousSet.create(Range.closed(first, last), DiscreteDomain.integers())); + } + + synchronized int next() throws InterruptedException { + if (freePorts.isEmpty()) { + wait(600_000); + if (freePorts.isEmpty()) { + throw new RuntimeException("no more ports in range " + first + "-" + last); + } + } + int port = freePorts.pop(); + takenPorts.add(port); + return port; + } + + synchronized void release(int port) { + if (port < first || port > last) { + throw new RuntimeException("trying to release port outside valid range " + port); + } + if (!takenPorts.contains(port)) { + throw new RuntimeException("trying to release port never acquired " + port); + } + takenPorts.remove(port); + freePorts.push(port); + notify(); + } + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java new file mode 100644 index 00000000000..d205bfe05e6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/RpcServerTest.java @@ -0,0 +1,125 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.base.Joiner; +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.jrt.Request; +import com.yahoo.vespa.config.*; +import com.yahoo.vespa.config.protocol.*; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.util.ConfigUtils; + +import com.yahoo.vespa.model.VespaModel; +import org.junit.*; +import org.junit.rules.TemporaryFolder; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Optional; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.1 + */ +public class RpcServerTest extends TestWithRpc { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testRpcServer() throws IOException, SAXException, InterruptedException { + testPrintStatistics(); + testGetConfig(); + testEnabled(); + testEmptyConfigHostedVespa(); + } + + + private void testEmptyConfigHostedVespa() throws InterruptedException { + rpcServer.onTenantDelete(TenantName.defaultName()); + rpcServer.onTenantsLoaded(); + JRTClientConfigRequest clientReq = createSimpleRequest(); + performRequest(clientReq.getRequest()); + assertFalse(clientReq.validateResponse()); + assertThat(clientReq.errorCode(), is(ErrorCode.APPLICATION_NOT_LOADED)); + stopRpc(); + createAndStartRpcServer(true); + rpcServer.onTenantsLoaded(); + clientReq = createSimpleRequest(); + performRequest(clientReq.getRequest()); + assertTrue(clientReq.validateResponse()); + } + + private JRTClientConfigRequest createSimpleRequest() { + ConfigKey<?> key = new ConfigKey<>(SimpletypesConfig.class, ""); + JRTClientConfigRequest clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, SimpletypesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty()); + assertTrue(clientReq.validateParameters()); + return clientReq; + } + + + private void testEnabled() throws IOException, SAXException { + generationCounter.increment(); + Application app = new Application(new VespaModel(MockApplicationPackage.createEmpty()), new ServerCache(), 2l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()); + ApplicationSet appSet = ApplicationSet.fromSingle(app); + rpcServer.configReloaded(TenantName.defaultName(), appSet); + ConfigKey<?> key = new ConfigKey<>(LbServicesConfig.class, "*"); + JRTClientConfigRequest clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, LbServicesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty()); + assertTrue(clientReq.validateParameters()); + performRequest(clientReq.getRequest()); + assertFalse(clientReq.validateResponse()); + assertThat(clientReq.errorCode(), is(ErrorCode.APPLICATION_NOT_LOADED)); + + rpcServer.onTenantsLoaded(); + clientReq = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, LbServicesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty()); + assertTrue(clientReq.validateParameters()); + performRequest(clientReq.getRequest()); + boolean validResponse = clientReq.validateResponse(); + assertTrue(clientReq.errorMessage(), validResponse); + assertThat(clientReq.errorCode(), is(0)); + } + + public void testGetConfig() { + tenantProvider.requestHandler.throwException = false; + ConfigKey<?> key = new ConfigKey<>(SimpletypesConfig.class, "brim"); + tenantProvider.requestHandler.responses.put(ApplicationId.defaultId(), createResponse()); + JRTClientConfigRequest req = JRTClientConfigRequestV3.createFromRaw(new RawConfig(key, SimpletypesConfig.CONFIG_DEF_MD5), 120_000, Trace.createDummy(), CompressionType.UNCOMPRESSED, Optional.empty()); + assertTrue(req.validateParameters()); + performRequest(req.getRequest()); + assertThat(req.errorCode(), is(0)); + assertTrue(req.validateResponse()); + ConfigPayload payload = ConfigPayload.fromUtf8Array(req.getNewPayload().getData()); + assertNotNull(payload); + SimpletypesConfig.Builder builder = new SimpletypesConfig.Builder(); + new ConfigPayloadApplier<>(builder).applyPayload(payload); + SimpletypesConfig config = new SimpletypesConfig(builder); + assertThat(config.intval(), is(123)); + } + + public ConfigResponse createResponse() { + SimpletypesConfig.Builder builder = new SimpletypesConfig.Builder(); + builder.intval(123); + SimpletypesConfig responseConfig = new SimpletypesConfig(builder); + ConfigPayload responsePayload = ConfigPayload.fromInstance(responseConfig); + InnerCNode targetDef = new DefParser(SimpletypesConfig.CONFIG_DEF_NAME, new StringReader(Joiner.on("\n").join(SimpletypesConfig.CONFIG_DEF_SCHEMA))).getTree(); + return SlimeConfigResponse.fromConfigPayload(responsePayload, targetDef, 3l, ConfigUtils.getMd5(responsePayload)); + } + + public void testPrintStatistics() { + Request req = new Request("printStatistics"); + rpcServer.printStatistics(req); + assertThat(req.returnValues().get(0).asString(), is("Delayed responses queue size: 0")); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java new file mode 100644 index 00000000000..52179b2cf48 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ServerCacheTest.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.config.ConfigCacheKey; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class ServerCacheTest { + private ServerCache cache; + + private static String defMd5 = "595f44fec1e92a71d3e9e77456ba80d1"; + private static String defMd5_2 = "a2f8edfc965802bf6d44826f9da7e2b0"; + private static String configMd5 = "mymd5"; + private static String configMd5_2 = "mymd5_2"; + private static ConfigDefinition payload = new ConfigDefinition("mypayload", new String[0]); + private static ConfigDefinition payload_2 = new ConfigDefinition("otherpayload", new String[0]); + + private static ConfigDefinitionKey fooBarDefKey = new ConfigDefinitionKey("foo", "bar"); + private static ConfigDefinitionKey fooBazDefKey = new ConfigDefinitionKey("foo", "baz"); + private static ConfigDefinitionKey fooBimDefKey = new ConfigDefinitionKey("foo", "bim"); + + private static ConfigKey<?> fooConfigKey = new ConfigKey<>("foo", "id", "bar"); + private static ConfigKey<?> bazConfigKey = new ConfigKey<>("foo", "id2", "bar"); + + ConfigCacheKey fooBarCacheKey = new ConfigCacheKey(fooConfigKey, defMd5); + ConfigCacheKey bazQuuxCacheKey = new ConfigCacheKey(bazConfigKey, defMd5); + ConfigCacheKey fooBarCacheKeyDifferentMd5 = new ConfigCacheKey(fooConfigKey, defMd5_2); + + @Before + public void setup() { + cache = new ServerCache(); + + cache.addDef(fooBarDefKey, payload); + cache.addDef(fooBazDefKey, new com.yahoo.vespa.config.buildergen.ConfigDefinition("baz", new String[0])); + + cache.addDef(fooBimDefKey, new ConfigDefinition("mynode", new String[0])); + + cache.put(fooBarCacheKey, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload.getCNode(), 2, configMd5), configMd5); + cache.put(bazQuuxCacheKey, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload.getCNode(), 2, configMd5), configMd5); + cache.put(fooBarCacheKeyDifferentMd5, SlimeConfigResponse.fromConfigPayload(ConfigPayload.empty(), payload_2.getCNode(), 2, configMd5_2), configMd5_2); + } + + @Test + public void testThatCacheWorks() { + assertNotNull(cache.getDef(fooBazDefKey)); + assertThat(cache.getDef(fooBarDefKey), is(payload)); + assertThat(cache.getDef(fooBimDefKey).getCNode().getName(), is("mynode")); + ConfigResponse raw = cache.get(fooBarCacheKey); + assertThat(raw.getConfigMd5(), is(configMd5)); + } + + @Test + public void testThatCacheWorksWithSameKeyDifferentMd5() { + assertThat(cache.getDef(fooBarDefKey), is(payload)); + ConfigResponse raw = cache.get(fooBarCacheKey); + assertThat(raw.getConfigMd5(), is(configMd5)); + raw = cache.get(fooBarCacheKeyDifferentMd5); + assertThat(raw.getConfigMd5(), is(configMd5_2)); + } + + @Test + public void testThatCacheWorksWithDifferentKeySameMd5() { + assertTrue(cache.get(fooBarCacheKey) == cache.get(bazQuuxCacheKey)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java new file mode 100644 index 00000000000..a69cf547db0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelControllerTest.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.model.VespaModel; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import com.yahoo.cloud.config.ElkConfig; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.9 + */ +public class SuperModelControllerTest { + + private static final File testApp = new File("src/test/resources/deploy/app"); + private SuperModelGenerationCounter counter; + private SuperModelController controller; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void setup() throws IOException { + counter = new SuperModelGenerationCounter(new MockCurator()); + controller = new SuperModelController(counter, + new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder()), + new ElkConfig(new ElkConfig.Builder())); + } + + @Test + public void test_super_model_reload() throws IOException, SAXException { + TenantName tenantA = TenantName.from("a"); + assertNotNull(controller.getHandler()); + long gen = counter.increment(); + controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1)); + assertNotNull(controller.getHandler()); + assertThat(controller.getHandler().getGeneration(), is(gen)); + controller.reloadConfig(tenantA, createApp(tenantA, "foo", 4l, 2)); + assertThat(controller.getHandler().getGeneration(), is(gen)); + // Test that a new app is used when there already exist an application with the same id + ApplicationId appId = new ApplicationId.Builder().tenant(tenantA).applicationName("foo").build(); + assertThat(((TestApplication) controller.getHandler().getSuperModel().getCurrentModels().get(tenantA).get(appId)).version, is(2l)); + gen = counter.increment(); + controller.reloadConfig(tenantA, createApp(tenantA, "bar", 2l, 3)); + assertThat(controller.getHandler().getGeneration(), is(gen)); + } + + @Test + public void test_super_model_remove() throws IOException, SAXException { + TenantName tenantA = TenantName.from("a"); + TenantName tenantB = TenantName.from("b"); + long gen = counter.increment(); + controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1)); + controller.reloadConfig(tenantA, createApp(tenantA, "bar", 30l, 2)); + controller.reloadConfig(tenantB, createApp(tenantB, "baz", 9l, 3)); + assertThat(controller.getHandler().getGeneration(), is(gen)); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2)); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(2)); + controller.removeApplication( + new ApplicationId.Builder().tenant("a").applicationName("unknown").build()); + assertThat(controller.getHandler().getGeneration(), is(gen)); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2)); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(2)); + gen = counter.increment(); + controller.removeApplication( + new ApplicationId.Builder().tenant("a").applicationName("bar").build()); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().size(), is(2)); + assertThat(controller.getHandler().getSuperModel().getCurrentModels().get(TenantName.from("a")).size(), is(1)); + assertThat(controller.getHandler().getGeneration(), is(gen)); + } + + @Test + public void test_super_model_master_generation() throws IOException, SAXException { + TenantName tenantA = TenantName.from("a"); + long masterGen = 10; + controller = new SuperModelController(counter, + new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder().masterGeneration(masterGen)), + new ElkConfig(new ElkConfig.Builder())); + + long gen = counter.increment(); + controller.reloadConfig(tenantA, createApp(tenantA, "foo", 3l, 1)); + assertThat(controller.getHandler().getGeneration(), is(masterGen + gen)); + } + + @Test + public void test_super_model_has_application_when_enabled() { + assertFalse(controller.hasApplication(ApplicationId.global(), Optional.empty())); + controller.enable(); + assertTrue(controller.hasApplication(ApplicationId.global(), Optional.empty())); + } + + private ApplicationSet createApp(TenantName tenant, String application, long generation, long version) throws IOException, SAXException { + return ApplicationSet.fromSingle( + new TestApplication( + new VespaModel(FilesApplicationPackage.fromFile(testApp)), + new ServerCache(), + generation, + new ApplicationId.Builder().tenant(tenant).applicationName(application).build(), + version)); + } + + private static class TestApplication extends Application { + private long version = 0; + + public TestApplication(VespaModel vespaModel, ServerCache cache, long appGeneration, ApplicationId app, long version) { + super(vespaModel, cache, appGeneration, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), app); + this.version = version; + } + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java new file mode 100644 index 00000000000..93a48094fac --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/SuperModelRequestHandlerTest.java @@ -0,0 +1,203 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.cloud.config.ElkConfig; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.*; +import com.yahoo.jrt.Request; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.cloud.config.LbServicesConfig.Tenants.Applications; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; +import com.yahoo.vespa.config.protocol.Trace; +import com.yahoo.vespa.config.protocol.VespaVersion; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.model.SuperModel; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.model.VespaModel; + +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import com.yahoo.cloud.config.ElkConfig.Logstash; + +import com.yahoo.vespa.config.server.model.ElkProducer; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.9 + */ +public class SuperModelRequestHandlerTest { + + private SuperModelRequestHandler handler; + + @Before + public void setupHandler() throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> models = new LinkedHashMap<>(); + models.put(TenantName.from("a"), new LinkedHashMap<>()); + File testApp = new File("src/test/resources/deploy/app"); + ApplicationId app = ApplicationId.from(TenantName.from("a"), + ApplicationName.from("foo"), InstanceName.defaultName()); + models.get(app.tenant()).put(app, new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp)), new ServerCache(), 4l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), app)); + handler = new SuperModelRequestHandler(new SuperModel(models, new ElkConfig(new ElkConfig.Builder()), Zone.defaultZone()), new TestConfigDefinitionRepo(), 2, new UncompressedConfigResponseFactory()); + } + + @Test + public void test_super_model_resolve_elk() { + ConfigResponse response = handler.resolveConfig(new GetConfigRequest() { + @Override + public ConfigKey<?> getConfigKey() { + return new ConfigKey<>(ElkConfig.class, "dontcare"); + } + + @Override + public DefContent getDefContent() { + return DefContent.fromClass(ElkConfig.class); + } + + @Override + public Optional<VespaVersion> getVespaVersion() { + return Optional.empty(); + } + + @Override + public boolean noCache() { + return false; + } + }); + assertThat(response.getGeneration(), is(2l)); + } + + @Test + public void test_lb_config_simple() { + LbServicesConfig.Builder lb = new LbServicesConfig.Builder(); + handler.getSuperModel().getConfig(lb); + LbServicesConfig lbc = new LbServicesConfig(lb); + assertThat(lbc.tenants().size(), is(1)); + assertThat(lbc.tenants("a").applications().size(), is(1)); + Applications app = lbc.tenants("a").applications("foo:prod:default:default"); + assertTrue(app.hosts().size() > 0); + } + + + @Test(expected = UnknownConfigDefinitionException.class) + public void test_unknown_config_definition() { + String md5 = "asdfasf"; + Request request = JRTClientConfigRequestV3.createWithParams(new ConfigKey<>("foo", "id", "bar", md5, null), DefContent.fromList(Collections.emptyList()), + "fromHost", md5, 1, 1, Trace.createDummy(), CompressionType.UNCOMPRESSED, + Optional.empty()) + .getRequest(); + JRTServerConfigRequestV3 v3Request = JRTServerConfigRequestV3.createFromRequest(request); + handler.resolveConfig(v3Request); + } + + @Test + public void test_lb_config_multiple_apps() throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> models = new LinkedHashMap<>(); + models.put(TenantName.from("t1"), new LinkedHashMap<>()); + models.put(TenantName.from("t2"), new LinkedHashMap<>()); + File testApp1 = new File("src/test/resources/deploy/app"); + File testApp2 = new File("src/test/resources/deploy/advancedapp"); + File testApp3 = new File("src/test/resources/deploy/advancedapp"); + // TODO must fix equals, hashCode on Tenant + Version vespaVersion = Version.fromIntValues(1, 2, 3); + models.get(TenantName.from("t1")).put(applicationId("mysimpleapp"), + new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp1)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("mysimpleapp"))); + models.get(TenantName.from("t1")).put(applicationId("myadvancedapp"), + new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp2)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("myadvancedapp"))); + models.get(TenantName.from("t2")).put(applicationId("minetooadvancedapp"), + new Application(new VespaModel(FilesApplicationPackage.fromFile(testApp3)), new ServerCache(), 4l, vespaVersion, MetricUpdater.createTestUpdater(), applicationId("minetooadvancedapp"))); + + SuperModelRequestHandler han = new SuperModelRequestHandler(new SuperModel(models, new ElkConfig(new ElkConfig.Builder()), Zone.defaultZone()), new TestConfigDefinitionRepo(), 2, new UncompressedConfigResponseFactory()); + LbServicesConfig.Builder lb = new LbServicesConfig.Builder(); + han.getSuperModel().getConfig(lb); + LbServicesConfig lbc = new LbServicesConfig(lb); + assertThat(lbc.tenants().size(), is(2)); + assertThat(lbc.tenants("t1").applications().size(), is(2)); + assertThat(lbc.tenants("t2").applications().size(), is(1)); + assertThat(lbc.tenants("t2").applications("minetooadvancedapp:prod:default:default").hosts().size(), is(1)); + assertQrServer(lbc.tenants("t2").applications("minetooadvancedapp:prod:default:default")); + } + + private ApplicationId applicationId(String applicationName) { + return ApplicationId.from(TenantName.defaultName(), + ApplicationName.from(applicationName), InstanceName.defaultName()); + } + + private void assertQrServer(Applications app) { + String host = app.hosts().keySet().iterator().next(); + Applications.Hosts hosts = app.hosts(host); + assertThat(hosts.hostname(), is(host)); + for (Map.Entry<String, Applications.Hosts.Services> e : app.hosts(host).services().entrySet()) { + System.out.println(e); + if ("qrserver".equals(e.getKey())) { + Applications.Hosts.Services s = e.getValue(); + assertThat(s.type(), is("qrserver")); + assertThat(s.ports().size(), is(4)); + assertThat(s.index(), is(0)); + return; + } + } + org.junit.Assert.fail("No qrserver service in config"); + } + + @Test + public void testElkConfig() { + ElkConfig ec = new ElkConfig(new ElkConfig.Builder().elasticsearch(new ElkConfig.Elasticsearch.Builder().host("es1").port(99)). + logstash(new ElkConfig.Logstash.Builder(). + config_file("/cfgfile"). + source_field("srcfield"). + spool_size(345). + network(new Logstash.Network.Builder(). + servers(new Logstash.Network.Servers.Builder(). + host("ls1"). + port(999)). + servers(new Logstash.Network.Servers.Builder(). + host("ls2"). + port(998)). + timeout(78)). + files(new ElkConfig.Logstash.Files.Builder(). + paths("path1"). + paths("path2"). + fields("field1", "f1val"). + fields("field2", "f2val")))); + ElkProducer ep = new ElkProducer(ec); + ElkConfig.Builder newBuilder = new ElkConfig.Builder(); + ep.getConfig(newBuilder); + ElkConfig elkConfig = new ElkConfig(newBuilder); + assertThat(elkConfig.elasticsearch(0).host(), is("es1")); + assertThat(elkConfig.elasticsearch(0).port(), is(99)); + assertThat(elkConfig.logstash().network().servers(0).host(), is("ls1")); + assertThat(elkConfig.logstash().network().servers(0).port(), is(999)); + assertThat(elkConfig.logstash().network().servers(1).host(), is("ls2")); + assertThat(elkConfig.logstash().network().servers(1).port(), is(998)); + assertThat(elkConfig.logstash().network().timeout(), is(78)); + assertThat(elkConfig.logstash().config_file(), is("/cfgfile")); + assertThat(elkConfig.logstash().source_field(), is("srcfield")); + assertThat(elkConfig.logstash().spool_size(), is(345)); + assertThat(elkConfig.logstash().files().size(), is(1)); + assertThat(elkConfig.logstash().files(0).paths(0), is("path1")); + assertThat(elkConfig.logstash().files(0).paths(1), is("path2")); + assertThat(elkConfig.logstash().files(0).fields("field1"), is("f1val")); + assertThat(elkConfig.logstash().files(0).fields("field2"), is("f2val")); + } + } + + + diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java new file mode 100644 index 00000000000..94e08fdf1b3 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantRequestHandlerTest.java @@ -0,0 +1,300 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.model.application.provider.MockFileRegistry; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.VespaVersion; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; +import com.yahoo.vespa.config.server.model.TestModelFactory; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.session.RemoteSession; +import com.yahoo.vespa.config.server.session.SessionZooKeeperClient; +import com.yahoo.vespa.model.VespaModel; +import com.yahoo.vespa.model.VespaModelFactory; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.1 + */ +public class TenantRequestHandlerTest extends TestWithCurator { + + private static final Version vespaVersion = new VespaModelFactory(new NullConfigModelRegistry()).getVersion(); + private TenantRequestHandler server; + private MockReloadListener listener = new MockReloadListener(); + private File app1 = new File("src/test/apps/cs1"); + private File app2 = new File("src/test/apps/cs2"); + private TenantName tenant = TenantName.from("mytenant"); + private TestComponentRegistry componentRegistry; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void setUp() throws IOException, SAXException { + feedApp(app1, 1); + Metrics sh = Metrics.createTestMetrics(); + List<ReloadListener> listeners = new ArrayList<>(); + listeners.add(listener); + server = new TenantRequestHandler(sh, tenant, listeners, new UncompressedConfigResponseFactory(), new HostRegistries()); + componentRegistry = new TestComponentRegistry(curator, configCurator, createRegistry()); + } + + private void feedApp(File appDir, long sessionId) throws IOException { + feedApp(appDir, sessionId, new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build()); + } + + private void feedApp(File appDir, long sessionId, ApplicationId appId) throws IOException { + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(sessionId), new TestConfigDefinitionRepo(), ""); + zkc.writeApplicationId(appId); + File app = tempFolder.newFolder(); + IOUtils.copyDirectory(appDir, app); + ZooKeeperDeployer deployer = zkc.createDeployer(new BaseDeployLogger()); + deployer.deploy(FilesApplicationPackage.fromFile(appDir), Collections.singletonMap(vespaVersion, new MockFileRegistry()), Collections.emptyMap()); + } + + private ApplicationSet reloadConfig(long id) { + return reloadConfig(id, "default"); + } + + private ApplicationSet reloadConfig(long id, String application) { + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(id), new TestConfigDefinitionRepo(), ""); + zkc.writeApplicationId(new ApplicationId.Builder().tenant(tenant).applicationName(application).build()); + RemoteSession session = new RemoteSession(tenant, id, componentRegistry, zkc); + return session.ensureApplicationLoaded(); + } + + private ModelFactoryRegistry createRegistry() { + return new ModelFactoryRegistry(Arrays.asList(new TestModelFactory(vespaVersion), + new TestModelFactory(Version.fromIntValues(3, 2, 1)))); + } + + public <T extends ConfigInstance> T resolve(Class<T> clazz, TenantRequestHandler tenantRequestHandler, String configId) { + return resolve(clazz, tenantRequestHandler, new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build(), vespaVersion, configId); + } + + public <T extends ConfigInstance> T resolve(final Class<T> clazz, TenantRequestHandler tenantRequestHandler, ApplicationId appId, Version vespaVersion, final String configId) { + ConfigResponse response = tenantRequestHandler.resolveConfig(appId, new GetConfigRequest() { + @Override + public ConfigKey<T> getConfigKey() { + return new ConfigKey<T>(clazz, configId); + } + + @Override + public DefContent getDefContent() { + return DefContent.fromClass(clazz); + } + + @Override + public Optional<VespaVersion> getVespaVersion() { + return Optional.of(VespaVersion.fromString(vespaVersion.toSerializedForm())); + } + + @Override + public boolean noCache() { + return false; + } + }, Optional.empty()); + return ConfigPayload.fromUtf8Array(response.getPayload()).toInstance(clazz, configId); + } + + @Test + public void testReloadConfig() throws IOException, SAXException { + ApplicationId applicationId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build(); + server.reloadConfig(reloadConfig(1)); + assertThat(listener.reloaded.get(), is(1)); + // Using only payload list for this simple test + SimpletypesConfig config = resolve(SimpletypesConfig.class, server, ""); + assertThat(config.intval(), is(1337)); + assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(1l)); + + server.reloadConfig(reloadConfig(1l)); + config = resolve(SimpletypesConfig.class, server, ""); + assertThat(config.intval(), is(1337)); + assertThat(listener.reloaded.get(), is(2)); + assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(1l)); + assertThat(listener.tenantHosts.size(), is(1)); + assertThat(server.resolveApplicationId("mytesthost"), is(applicationId)); + + listener.reloaded.set(0); + feedApp(app2, 2); + server.reloadConfig(reloadConfig(2l)); + config = resolve(SimpletypesConfig.class, server, ""); + assertThat(config.intval(), is(1330)); + assertThat(listener.reloaded.get(), is(1)); + assertThat(server.getApplicationGeneration(applicationId, Optional.of(vespaVersion)), is(2l)); + } + + @Test + public void testRemoveApplication() { + server.reloadConfig(reloadConfig(1)); + assertThat(listener.removed.get(), is(0)); + server.removeApplication(new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build()); + assertThat(listener.removed.get(), is(1)); + } + + @Test + public void testResolveForAppId() { + long id = 1l; + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, configCurator, new PathProvider(Path.createRoot()).getSessionDir(id), new TestConfigDefinitionRepo(), ""); + ApplicationId appId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("myapp").instanceName("myinst").build(); + zkc.writeApplicationId(appId); + RemoteSession session = new RemoteSession(appId.tenant(), id, componentRegistry, zkc); + server.reloadConfig(session.ensureApplicationLoaded()); + SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId, vespaVersion, ""); + assertThat(config.intval(), is(1337)); + } + + @Test + public void testResolveMultipleApps() throws IOException, SAXException { + ApplicationId appId1 = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("myapp1").instanceName("myinst1").build(); + ApplicationId appId2 = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("myapp2").instanceName("myinst2").build(); + feedAndReloadApp(app1, 1, appId1); + SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId1, vespaVersion, ""); + assertThat(config.intval(), is(1337)); + + feedAndReloadApp(app2, 2, appId2); + config = resolve(SimpletypesConfig.class, server, appId2, vespaVersion, ""); + assertThat(config.intval(), is(1330)); + } + + @Test + public void testResolveMultipleVersions() throws IOException { + ApplicationId appId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("myapp1").instanceName("myinst1").build(); + feedAndReloadApp(app1, 1, appId); + SimpletypesConfig config = resolve(SimpletypesConfig.class, server, appId, vespaVersion, ""); + assertThat(config.intval(), is(1337)); + config = resolve(SimpletypesConfig.class, server, appId, Version.fromIntValues(3, 2, 1), ""); + assertThat(config.intval(), is(1337)); + } + + private void feedAndReloadApp(File appDir, long sessionId, ApplicationId appId) throws IOException { + feedApp(appDir, sessionId, appId); + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, new PathProvider(Path.createRoot()).getSessionDir(sessionId)); + zkc.writeApplicationId(appId); + RemoteSession session = new RemoteSession(tenant, sessionId, componentRegistry, zkc); + server.reloadConfig(session.ensureApplicationLoaded()); + } + + public static class MockReloadListener implements ReloadListener { + public AtomicInteger reloaded = new AtomicInteger(0); + public AtomicInteger removed = new AtomicInteger(0); + public Map<String, Collection<String>> tenantHosts = new LinkedHashMap<>(); + @Override + public void configReloaded(TenantName tenant, ApplicationSet application) { + reloaded.incrementAndGet(); + } + + @Override + public void hostsUpdated(TenantName tenant, Collection<String> newHosts) { + tenantHosts.put(tenant.value(), newHosts); + } + + @Override + public void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts) { + } + + @Override + public void applicationRemoved(ApplicationId applicationId) { + removed.incrementAndGet(); + } + } + + @Test + public void testHasApplication() throws IOException, SAXException { + assertdefaultAppNotFound(); + server.reloadConfig(reloadConfig(1l)); + assertTrue(server.hasApplication(new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(tenant).build(), Optional.of(vespaVersion))); + } + + private void assertdefaultAppNotFound() { + assertFalse(server.hasApplication(ApplicationId.defaultId(), Optional.of(vespaVersion))); + } + + @Test + public void testMultipleApplicationsReload() { + assertdefaultAppNotFound(); + server.reloadConfig(reloadConfig(1l, "foo")); + assertdefaultAppNotFound(); + assertTrue(server.hasApplication(new ApplicationId.Builder().tenant(tenant).applicationName("foo").build(), + Optional.of(vespaVersion))); + assertThat(server.resolveApplicationId("doesnotexist"), is(ApplicationId.defaultId())); + assertThat(server.resolveApplicationId("mytesthost"), is(new ApplicationId.Builder() + .tenant(tenant) + .applicationName("foo").build())); // Host set in application package. + } + + @Test + public void testListConfigs() throws IOException, SAXException { + assertdefaultAppNotFound(); + /*assertTrue(server.allConfigIds(ApplicationId.defaultId()).isEmpty()); + assertTrue(server.allConfigsProduced(ApplicationId.defaultId()).isEmpty()); + assertTrue(server.listConfigs(ApplicationId.defaultId(), false).isEmpty()); + assertTrue(server.listConfigs(ApplicationId.defaultId(), true).isEmpty());*/ + + VespaModel model = new VespaModel(FilesApplicationPackage.fromFile(new File("src/test/apps/app"))); + server.reloadConfig(ApplicationSet.fromSingle(new Application(model, new ServerCache(), 1, vespaVersion, MetricUpdater.createTestUpdater(), ApplicationId.defaultId()))); + Set<ConfigKey<?>> configNames = server.listConfigs(ApplicationId.defaultId(), Optional.of(vespaVersion), false); + assertTrue(configNames.contains(new ConfigKey<>("sentinel", "hosts", "cloud.config"))); + //for (ConfigKey<?> ck : configNames) { + // assertTrue(!"".equals(ck.getConfigId())); + //} + + configNames = server.listConfigs(ApplicationId.defaultId(), Optional.of(vespaVersion), true); + System.out.println(configNames); + assertTrue(configNames.contains(new ConfigKey<>("feeder", "jdisc", "vespaclient.config"))); + assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "jdisc", "document.config"))); + assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "", "document.config"))); + assertTrue(configNames.contains(new ConfigKey<>("documenttypes", "", "document"))); + assertTrue(configNames.contains(new ConfigKey<>("documentmanager", "jdisc", "document.config"))); + assertTrue(configNames.contains(new ConfigKey<>("health-monitor", "jdisc", "container.jdisc.config"))); + assertTrue(configNames.contains(new ConfigKey<>("specific", "jdisc", "project"))); + } + + @Test + public void testAppendIdsInNonRecursiveListing() { + assertEquals(server.appendOneLevelOfId("search/music", "search/music/qrservers/default/qr.0"), "search/music/qrservers"); + assertEquals(server.appendOneLevelOfId("search", "search/music/qrservers/default/qr.0"), "search/music"); + assertEquals(server.appendOneLevelOfId("search/music/qrservers/default/qr.0", "search/music/qrservers/default/qr.0"), "search/music/qrservers/default/qr.0"); + assertEquals(server.appendOneLevelOfId("", "search/music/qrservers/default/qr.0"), "search"); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java new file mode 100644 index 00000000000..c4eb8786917 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantTest.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.testing.EqualsTester; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.http.v2.TestTenantBuilder; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.3 + */ +public class TenantTest extends TestWithCurator { + + private Tenant t1; + private Tenant t2; + private Tenant t3; + private Tenant t4; + + @Before + public void setupTenant() throws Exception { + t1 = createTenant("foo"); + t2 = createTenant("foo"); + t3 = createTenant("bar"); + t4 = createTenant("baz"); + } + + private Tenant createTenant(String name) throws Exception { + return new TestTenantBuilder().createTenant(TenantName.from(name)).build(); + } + + @Test + public void equals() { + new EqualsTester() + .addEqualityGroup(t1, t2) + .addEqualityGroup(t3) + .addEqualityGroup(t4) + .testEquals(); + } + + @Test + public void hashcode() { + assertThat(t1.hashCode(), is(t2.hashCode())); + assertThat(t1.hashCode(), is(not(t3.hashCode()))); + assertThat(t1.hashCode(), is(not(t4.hashCode()))); + } + + @Test + public void close() { + MemoryApplicationRepo repo = (MemoryApplicationRepo) t1.getApplicationRepo(); + assertTrue(repo.isOpen()); + t1.close(); + assertFalse(repo.isOpen()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java new file mode 100644 index 00000000000..9f71393115f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TenantsTestCase.java @@ -0,0 +1,181 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.deploy.MockDeployer; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.model.VespaModel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TenantsTestCase extends TestWithCurator { + private Tenants tenants; + TestComponentRegistry globalComponentRegistry; + private TenantRequestHandlerTest.MockReloadListener listener; + private MockTenantListener tenantListener; + private final TenantName tenant1 = TenantName.from("tenant1"); + private final TenantName tenant2 = TenantName.from("tenant2"); + private final TenantName tenant3 = TenantName.from("tenant3"); + + @Before + public void setupSessions() throws Exception { + globalComponentRegistry = new TestComponentRegistry(curator); + listener = globalComponentRegistry.reloadListener; + tenantListener = globalComponentRegistry.tenantListener; + tenantListener.tenantsLoaded = false; + tenants = new Tenants(globalComponentRegistry, Metrics.createTestMetrics()); + assertTrue(tenantListener.tenantsLoaded); + tenants.createTenant(tenant1); + tenants.createTenant(tenant2); + } + + @After + public void closeSessions() throws IOException { + tenants.close(); + } + + @Test + public void testStartUp() { + assertEquals(tenants.tenantsCopy().get(tenant1).getName(), tenant1); + assertEquals(tenants.tenantsCopy().get(tenant2).getName(), tenant2); + } + + @Test + public void testListenersAdded() throws IOException, SAXException { + tenants.tenantsCopy().get(tenant1).getReloadHandler().reloadConfig(ApplicationSet.fromSingle(new Application(new VespaModel(MockApplicationPackage.createEmpty()), new ServerCache(), 4l, Version.fromIntValues(1, 2, 3), MetricUpdater.createTestUpdater(), ApplicationId.defaultId()))); + assertThat(listener.reloaded.get(), is(1)); + } + + private List<String> readZKChildren(String path) throws Exception { + return curator.framework().getChildren().forPath(path); + } + + @Test + public void testTenantListenersNotified() throws Exception { + tenants.createTenant(tenant3); + assertThat("tenant3 not the last created tenant. Tenants: " + tenants.tenantsCopy().keySet() + ", /config/v2/tenants: " + readZKChildren("/config/v2/tenants"), tenantListener.tenantCreatedName, is(tenant3)); + tenants.deleteTenant(tenant2); + assertFalse(tenants.tenantsCopy().containsKey(tenant2)); + assertThat(tenantListener.tenantDeletedName, is(tenant2)); + } + + @Test + public void testAddTenant() throws Exception { + Map<TenantName, Tenant> tenantsCopy = tenants.tenantsCopy(); + assertEquals(tenantsCopy.get(tenant1).getName(), tenant1); + assertEquals(tenantsCopy.get(tenant2).getName(), tenant2); + tenants.createTenant(tenant3); + tenantsCopy = tenants.tenantsCopy(); + assertEquals(tenantsCopy.get(tenant1).getName(), tenant1); + assertEquals(tenantsCopy.get(tenant2).getName(), tenant2); + assertEquals(tenantsCopy.get(tenant3).getName(), tenant3); + } + + @Test + public void testPutAdd() throws Exception { + tenants.createTenant(tenant3); + assertNotNull(globalComponentRegistry.getCurator().framework().checkExists().forPath(tenants.tenantZkPath(tenant3))); + } + + @Test + public void testRemove() throws Exception { + assertNotNull(globalComponentRegistry.getCurator().framework().checkExists().forPath(tenants.tenantZkPath(tenant1))); + tenants.deleteTenant(tenant1); + assertFalse(tenants.tenantsCopy().containsKey(tenant1)); + } + + @Test + public void testTenantsChanged() throws Exception { + tenants.close(); // close the Tenants instance created in setupSession, we do not want to use one with a PatchChildrenCache listener + tenants = new Tenants(globalComponentRegistry, Metrics.createTestMetrics(), new ArrayList<>()); + Set<TenantName> newTenants = new LinkedHashSet<>(); + TenantName defaultTenant = TenantName.defaultName(); + newTenants.add(tenant2); + newTenants.add(defaultTenant); + tenants.tenantsChanged(newTenants); + Map<TenantName, Tenant> tenantsCopy = tenants.tenantsCopy(); + assertEquals(tenantsCopy.get(tenant2).getName(), tenant2); + assertEquals(tenantsCopy.get(defaultTenant).getName().value(), "default"); + assertNull(tenantsCopy.get(tenant1)); + newTenants.clear(); + tenants.tenantsChanged(newTenants); + tenantsCopy = tenants.tenantsCopy(); + assertNull(tenantsCopy.get(tenant1)); + assertNull(tenantsCopy.get(tenant2)); + assertNull(tenantsCopy.get(defaultTenant)); + newTenants.clear(); + TenantName foo = TenantName.from("foo"); + TenantName bar = TenantName.from("bar"); + newTenants.add(tenant2); + newTenants.add(foo); + newTenants.add(bar); + tenants.tenantsChanged(newTenants); + tenantsCopy = tenants.tenantsCopy(); + assertNotNull(tenantsCopy.get(tenant2)); + assertNotNull(tenantsCopy.get(foo)); + assertNotNull(tenantsCopy.get(bar)); + assertEquals(tenantsCopy.get(tenant2).getName(), tenant2); + assertEquals(tenantsCopy.get(foo).getName(), foo); + assertEquals(tenantsCopy.get(bar).getName(), bar); + } + + @Test + public void testTenantWatching() throws Exception { + TestComponentRegistry reg = new TestComponentRegistry(curator); + Tenants t = new Tenants(reg, Metrics.createTestMetrics()); + try { + assertEquals(t.tenantsCopy().get(TenantName.defaultName()).getName(), TenantName.defaultName()); + reg.getCurator().framework().create().forPath(tenants.tenantZkPath(TenantName.from("newTenant"))); + // Poll for the watcher to pick up the tenant from zk, and add it + int tries=0; + while(true) { + if (tries > 500) fail("Didn't react on watch"); + Tenant nt = t.tenantsCopy().get(TenantName.from("newTenant")); + if (nt != null) { + assertEquals(nt.getName().value(), "newTenant"); + return; + } + tries++; + Thread.sleep(100); + } + } finally { + t.close(); + } + } + + @Test + public void testTenantRedeployment() throws Exception { + MockDeployer deployer = new MockDeployer(); + Tenant tenant = tenants.tenantsCopy().get(tenant1); + ApplicationId id = ApplicationId.from(tenant1, ApplicationName.defaultName(), InstanceName.defaultName()); + tenant.getApplicationRepo().createPutApplicationTransaction(id, 3).commit(); + tenants.redeployApplications(deployer); + assertThat(deployer.lastDeployed, is(id)); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java new file mode 100644 index 00000000000..d202f55e38a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestComponentRegistry.java @@ -0,0 +1,134 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.io.Files; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.FileDistributionFactory; +import com.yahoo.vespa.config.server.session.MockFileDistributionFactory; +import com.yahoo.vespa.config.server.session.SessionPreparer; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.model.VespaModelFactory; + +import java.util.Collections; +import java.util.Optional; + +/** + * @author lulf + * @since 5.1 + */ +// TODO Use a Builder to avoid so many constructors +public class TestComponentRegistry implements GlobalComponentRegistry { + + private final Curator curator; + private final ConfigCurator configCurator; + private final Metrics metrics; + private final ConfigServerDB serverDB; + private final SessionPreparer sessionPreparer; + private final ConfigserverConfig configserverConfig; + private final SuperModelGenerationCounter superModelGenerationCounter; + private final ConfigDefinitionRepo defRepo; + final TenantRequestHandlerTest.MockReloadListener reloadListener; + final MockTenantListener tenantListener; + private final PermanentApplicationPackage permanentApplicationPackage; + private final HostRegistries hostRegistries; + private final FileDistributionFactory fileDistributionFactory; + private final ModelFactoryRegistry modelFactoryRegistry; + private final Optional<Provisioner> hostProvisioner; + + public TestComponentRegistry() { this(new MockCurator()); } + + public TestComponentRegistry(Curator curator) { + this(curator, ConfigCurator.create(curator), new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())))); + } + + public TestComponentRegistry(Curator curator, ConfigCurator configCurator, FileDistributionFactory fileDistributionFactory) { + this(curator, configCurator, new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))), fileDistributionFactory); + } + + public TestComponentRegistry(Curator curator, ModelFactoryRegistry modelFactoryRegistry) { + this(curator, ConfigCurator.create(curator), modelFactoryRegistry, Optional.empty()); + } + + public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry) { + this(curator, configCurator, modelFactoryRegistry, Optional.empty()); + } + + public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, FileDistributionFactory fileDistributionFactory) { + this(curator, configCurator, modelFactoryRegistry, Optional.empty(), fileDistributionFactory); + } + + public TestComponentRegistry(Curator curator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage) { + this(curator, ConfigCurator.create(curator), modelFactoryRegistry, permanentApplicationPackage, new MockFileDistributionFactory()); + } + + public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage) { + this(curator, configCurator, modelFactoryRegistry, permanentApplicationPackage, new MockFileDistributionFactory()); + } + + public TestComponentRegistry(Curator curator, ConfigCurator configCurator, ModelFactoryRegistry modelFactoryRegistry, Optional<PermanentApplicationPackage> permanentApplicationPackage, FileDistributionFactory fileDistributionFactory) { + this.curator = curator; + this.configCurator = configCurator; + metrics = Metrics.createTestMetrics(); + configserverConfig = new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(Files.createTempDir().getAbsolutePath())); + serverDB = new ConfigServerDB(configserverConfig); + reloadListener = new TenantRequestHandlerTest.MockReloadListener(); + tenantListener = new MockTenantListener(); + this.superModelGenerationCounter = new SuperModelGenerationCounter(curator); + this.defRepo = new StaticConfigDefinitionRepo(); + this.permanentApplicationPackage = permanentApplicationPackage.orElse(new PermanentApplicationPackage(configserverConfig)); + this.hostRegistries = new HostRegistries(); + this.fileDistributionFactory = fileDistributionFactory; + this.modelFactoryRegistry = modelFactoryRegistry; + this.hostProvisioner = Optional.empty(); + sessionPreparer = new SessionPreparer(modelFactoryRegistry, fileDistributionFactory, HostProvisionerProvider.empty(), this.permanentApplicationPackage, configserverConfig, defRepo, curator, new Zone(configserverConfig)); + } + + @Override + public Curator getCurator() { return curator; } + @Override + public ConfigCurator getConfigCurator() { return configCurator; } + @Override + public Metrics getMetrics() { return metrics; } + @Override + public ConfigServerDB getServerDB() { return serverDB; } + @Override + public SessionPreparer getSessionPreparer() { return sessionPreparer; } + @Override + public ConfigserverConfig getConfigserverConfig() { return configserverConfig; } + @Override + public TenantListener getTenantListener() { return tenantListener; } + @Override + public ReloadListener getReloadListener() { return reloadListener; } + @Override + public SuperModelGenerationCounter getSuperModelGenerationCounter() { return superModelGenerationCounter; } + @Override + public ConfigDefinitionRepo getConfigDefinitionRepo() { return defRepo; } + @Override + public PermanentApplicationPackage getPermanentApplicationPackage() { return permanentApplicationPackage; } + @Override + public HostRegistries getHostRegistries() { return hostRegistries;} + @Override + public ModelFactoryRegistry getModelFactoryRegistry() { return modelFactoryRegistry; } + + @Override + public Optional<Provisioner> getHostProvisioner() { + return hostProvisioner; + } + + @Override + public Zone getZone() { + return Zone.defaultZone(); + } + + public FileDistributionFactory getFileDistributionFactory() { return fileDistributionFactory; } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java new file mode 100644 index 00000000000..2a0c78ce7a5 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestConfigDefinitionRepo.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author lulf + * @since 5. + */ +public class TestConfigDefinitionRepo implements ConfigDefinitionRepo { + private final Map<ConfigDefinitionKey, ConfigDefinition> repo = new LinkedHashMap<>(); + public TestConfigDefinitionRepo() { + repo.put(new ConfigDefinitionKey(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE), + new ConfigDefinition(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_SCHEMA)); + repo.put(new ConfigDefinitionKey(LbServicesConfig.CONFIG_DEF_NAME, LbServicesConfig.CONFIG_DEF_NAMESPACE), + new ConfigDefinition(LbServicesConfig.CONFIG_DEF_NAME, LbServicesConfig.CONFIG_DEF_SCHEMA)); + } + + @Override + public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() { + return repo; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java new file mode 100644 index 00000000000..6f68aa07634 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithCurator.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.apache.curator.framework.CuratorFramework; +import org.junit.Before; + +/** + * For tests that require a Curator instance + * + * @author lulf + * @since 5.16 + */ +public class TestWithCurator { + + protected ConfigCurator configCurator; + protected CuratorFramework curatorFramework; + protected Curator curator; + + @Before + public void setupZKProvider() throws Exception { + curator = new MockCurator(); + configCurator = ConfigCurator.create(curator); + curatorFramework = curator.framework(); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java new file mode 100644 index 00000000000..3e5431deccd --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithRpc.java @@ -0,0 +1,105 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.cloud.config.ElkConfig; +import com.yahoo.config.provision.TenantName; +import com.yahoo.jrt.Request; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Transport; +import com.yahoo.vespa.config.GenerationCounter; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import org.junit.After; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Test running rpc server. + * + * @author lulf + * @since 5.17 + */ +public class TestWithRpc { + + protected RpcServer rpcServer; + protected MockTenantProvider tenantProvider; + protected GenerationCounter generationCounter; + private Thread t; + private Supervisor sup; + private Spec spec; + private int port; + + private List<Integer> allocatedPorts; + + @Before + public void setupRpc() throws InterruptedException { + allocatedPorts = new ArrayList<>(); + port = allocatePort(); + spec = createSpec(port); + tenantProvider = new MockTenantProvider(); + generationCounter = new MemoryGenerationCounter(); + createAndStartRpcServer(false); + } + + @After + public void teardownPortAllocator() { + for (Integer port : allocatedPorts) { + PortRangeAllocator.releasePort(port); + } + } + + protected int allocatePort() throws InterruptedException { + int port = PortRangeAllocator.findAvailablePort(); + allocatedPorts.add(port); + return port; + } + + protected void createAndStartRpcServer(boolean hostedVespa) { + rpcServer = new RpcServer(new ConfigserverConfig(new ConfigserverConfig.Builder().rpcport(port).numthreads(1).maxgetconfigclients(1).hostedVespa(hostedVespa)), + new SuperModelController(generationCounter, + new TestConfigDefinitionRepo(), new ConfigserverConfig(new ConfigserverConfig.Builder()), new ElkConfig(new ElkConfig.Builder())), + Metrics.createTestMetrics(), new HostRegistries()); + rpcServer.onTenantCreate(TenantName.from("default"), tenantProvider); + t = new Thread(rpcServer); + t.start(); + sup = new Supervisor(new Transport()); + pingServer(); + } + + @After + public void stopRpc() throws InterruptedException { + rpcServer.stop(); + t.join(); + } + + private Spec createSpec(int port) { + return new Spec("tcp/localhost:" + port); + } + + private void pingServer() { + long endTime = System.currentTimeMillis() + 60_000; + Request req = new Request("ping"); + while (System.currentTimeMillis() < endTime) { + performRequest(req); + if (!req.isError() && req.returnValues().size() > 0 && req.returnValues().get(0).asInt32() == 0) { + break; + } + req = new Request("ping"); + } + assertFalse(req.isError()); + assertTrue(req.returnValues().size() > 0); + assertThat(req.returnValues().get(0).asInt32(), is(0)); + } + + protected void performRequest(Request req) { + sup.connect(spec).invokeSync(req, 120.0); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java new file mode 100644 index 00000000000..3892bf505b9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TestWithTenant.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.config.server.monitoring.Metrics; +import org.junit.Before; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Utility for a test using a single default tenant. + * + * @author lulf + * @since 5.35 + */ +public class TestWithTenant extends TestWithCurator { + + protected Tenants tenants; + protected Tenant tenant; + + @Before + public void setupTenant() throws Exception { + tenants = new Tenants(new TestComponentRegistry(curator), Metrics.createTestMetrics()); + tenant = tenants.defaultTenant(); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java new file mode 100644 index 00000000000..224a759896f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/TimeoutBudgetTest.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.test.ManualClock; +import org.junit.Test; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class TimeoutBudgetTest { + public static TimeoutBudget day() { + return new TimeoutBudget(new ManualClock(Instant.now()), Duration.ofDays(1)); + } + + @Test + public void testTimeLeft() { + ManualClock clock = new ManualClock(); + + TimeoutBudget budget = new TimeoutBudget(clock, Duration.ofMillis(7)); + assertThat(budget.timeLeft().toMillis(), is(7l)); + clock.advance(Duration.ofMillis(1)); + assertThat(budget.timeLeft().toMillis(), is(6l)); + clock.advance(Duration.ofMillis(5)); + assertThat(budget.timeLeft().toMillis(), is(1l)); + assertThat(budget.timeLeft().toMillis(), is(1l)); + clock.advance(Duration.ofMillis(1)); + assertThat(budget.timeLeft().toMillis(), is(0l)); + clock.advance(Duration.ofMillis(5)); + assertThat(budget.timeLeft().toMillis(), is(0l)); + + clock.advance(Duration.ofMillis(1)); + assertThat(budget.timesUsed(), is("[0 ms, 1 ms, 5 ms, 0 ms, 1 ms, 5 ms, total: 13 ms]")); + } + + @Test + public void testHasTimeLeft() { + ManualClock clock = new ManualClock(); + + TimeoutBudget budget = new TimeoutBudget(clock, Duration.ofMillis(7)); + assertThat(budget.hasTimeLeft(), is(true)); + clock.advance(Duration.ofMillis(1)); + assertThat(budget.hasTimeLeft(), is(true)); + clock.advance(Duration.ofMillis(5)); + assertThat(budget.hasTimeLeft(), is(true)); + assertThat(budget.hasTimeLeft(), is(true)); + clock.advance(Duration.ofMillis(1)); + assertThat(budget.hasTimeLeft(), is(false)); + clock.advance(Duration.ofMillis(5)); + assertThat(budget.hasTimeLeft(), is(false)); + + clock.advance(Duration.ofMillis(1)); + assertThat(budget.timesUsed(), is("[0 ms, 1 ms, 5 ms, 0 ms, 1 ms, 5 ms, total: 13 ms]")); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg new file mode 100644 index 00000000000..8b43ff9c793 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/a-music-indexer-correct.cfg @@ -0,0 +1,78 @@ +accesslog "/home/vespa/logs/vespa/foo.log" +partialsd "sd" +partialsd2 "global2" +asyncfetchocc 10 +a 0 +b 1 +c 2 +d 3 +e 4 +onlyindef 45 +listenport 13700 +rangecheck2 10 +rangecheck3 10 +kanon -78.56 +rangecheck1 10.0 +testref search/cluster.music/c0/r0/indexer.4 +testref2 some/babbel +mode BATCH +functionmodules[0] +storage[2] +storage[0].feeder[1] +storage[0].feeder[0] "test" +storage[1].id search/cluster.music/c0/r0/indexer.4 +storage[1].id2 pjatt +storage[1].feeder[2] +storage[1].feeder[0] "me" +storage[1].feeder[1] "now" +search[3] +search[0].feeder[1] +search[0].feeder[0] "foofeeder" +search[1].feeder[4] +search[1].feeder[0] "barfeeder1_1" +search[1].feeder[1] "barfeeder2" +search[1].feeder[2] "" +search[1].feeder[3] "barfeeder2_1" +search[2].feeder[2] +search[2].feeder[0] "" +search[2].feeder[1] "bazfeeder" +f[1] +f[0].a "A" +f[0].b "B" +f[0].c "C" +f[0].h "H" +f[0].f "F" +config[1] +config[0].role "rtx" +config[0].usewrapper false +config[0].id search/cluster.music/rtx/0 +routingtable[1] +routingtable[0].hop[3] +routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.music.indexing" +routingtable[0].hop[0].selector "docproc/cluster.music.indexing/*/chain.music.indexing" +routingtable[0].hop[0].recipient[0] +routingtable[0].hop[1].name "search/cluster.music" +routingtable[0].hop[1].selector "search/cluster.music/[SearchColumn]/[SearchRow]/feed-destination" +routingtable[0].hop[1].recipient[1] +routingtable[0].hop[1].recipient[0] "search/cluster.music/c0/r0/feed-destination" +routingtable[0].hop[2].selector "[DocumentRouteSelector]" +routingtable[0].hop[2].name "indexing" +routingtable[0].hop[2].recipient[1] +routingtable[0].hop[2].recipient[0] "search/cluster.music" +speciallog[1] +speciallog[0].filehandler.name "QueryAccessLog" +speciallog[0].filehandler.pattern "logs/vespa/qrs/QueryAccessLog.%Y%m%d%H%M%S" +speciallog[0].filehandler.rotation "0 1 ..." +speciallog[0].cachehandler.name "QueryAccessLog" +speciallog[0].name "QueryAccessLog" +speciallog[0].type "file" +speciallog[0].cachehandler.size 1000 +rulebase[4] +rulebase[0].name "cjk" +rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n" +rulebase[1].name "common" +rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n" +rulebase[2].name "egyik" +rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n" +rulebase[3].name "masik" +rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg new file mode 100644 index 00000000000..927ff8a26c9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/a-sports-indexer-correct.cfg @@ -0,0 +1,48 @@ +accesslog "/home/vespa/logs/vespa/foo.log" +partialsd "global" +partialsd2 "global2" +asyncfetchocc 10 +a 0 +b 1 +c 67 +d 89 +e 4 +onlyindef 45 +listenport 13700 +rangecheck2 10 +rangecheck3 10 +rangecheck1 10.0 +mode BATCH +functionmodules[0] +storage[0] +search[3] +search[0].feeder[1] +search[0].feeder[0] "foofeeder" +search[1].feeder[4] +search[1].feeder[0] "barfeeder1_1" +search[1].feeder[1] "sportsfeeder1" +search[1].feeder[2] "" +search[1].feeder[3] "barfeeder2_1" +search[2].feeder[2] +search[2].feeder[0] "" +search[2].feeder[1] "bazfeeder" +f[0] +config[0] +routingtable[0] +speciallog[1] +speciallog[0].filehandler.name "QueryAccessLog" +speciallog[0].filehandler.pattern "logs/vespa/qrs/QueryAccessLog.%Y%m%d%H%M%S" +speciallog[0].filehandler.rotation "0 1 ..." +speciallog[0].cachehandler.name "QueryAccessLog" +speciallog[0].name "QueryAccessLog" +speciallog[0].type "file" +speciallog[0].cachehandler.size 1000 +rulebase[4] +rulebase[0].name "cjk" +rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n" +rulebase[1].name "common" +rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n" +rulebase[2].name "egyik" +rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n" +rulebase[3].name "masik" +rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar Binary files differnew file mode 100644 index 00000000000..69f6e335092 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/components/testbundle.jar diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml new file mode 100644 index 00000000000..53d7c599817 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/app_stripped/services.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<services version="1.0"> + + <admin version="2.0"> + <adminserver hostalias="node1"/> + </admin> + + <content version="1.0"> + <redundancy>1</redundancy> + <documents> + <document type="music" mode="index"/> + </documents> + <nodes>> + <node hostalias="node1" distribution-key="0"/> + </nodes> + </content> +</services> diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java new file mode 100644 index 00000000000..eefa4b6176d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceCheckerTest.java @@ -0,0 +1,210 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.cloud.config.ModelConfig; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.xml.sax.SAXException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * @author lulf + */ +public class ApplicationConvergenceCheckerTest { + + private TenantName tenant = TenantName.from("mytenant"); + private ApplicationId appId = ApplicationId.from(tenant, ApplicationName.from("myapp"), InstanceName.from("myinstance")); + private ObjectMapper mapper = new ObjectMapper(); + private Application application; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void setup() throws IOException, SAXException, InterruptedException { + Model mockModel = new MockModel(1337); + application = new Application(mockModel, new ServerCache(), 3, Version.fromIntValues(0, 0, 0), MetricUpdater.createTestUpdater(), appId); + } + + private void assertJsonResponseEquals(HttpResponse httpResponse, String expected) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + httpResponse.render(out); + String response = out.toString(StandardCharsets.UTF_8.name()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonResponse = mapper.readTree(response); + JsonNode jsonExpected = mapper.readTree(expected); + if (jsonExpected.equals(jsonResponse)) { + return; + } + fail("Not equal, response is '" + response + "' expected '"+ expected + "'"); + } + + @Test + public void converge() throws IOException, SAXException { + ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":3}}")); + checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1))); + } + + @Test + public void convergeV2() throws IOException, SAXException { + ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":3}}")); + final HttpResponse httpResponse = checker.listConfigConvergence(application, URI.create("http://foo:234/serviceconvergence")); + assertThat(httpResponse.getStatus(), is(200)); + assertJsonResponseEquals(httpResponse, "{\"services\":[" + + "{\"port\":1337,\"host\":\"localhost\"," + + "\"url\":\"http://foo:234/serviceconvergence/localhost:1337\"," + + "\"type\":\"container\"}]," + + "\"debug\":{\"wantedVersion\":3}," + + "\"url\":\"http://foo:234/serviceconvergence\"}"); + final HttpResponse nodeHttpResponse = checker.nodeConvergenceCheck(application, "localhost:1337", URI.create("http://foo:234/serviceconvergence")); + assertThat(nodeHttpResponse.getStatus(), is(200)); + assertJsonResponseEquals(nodeHttpResponse, "{" + + "\"converged\":true," + + "\"debug\":{\"wantedGeneration\":3," + + "\"currentGeneration\":3," + + "\"host\":\"localhost:1337\"}," + + "\"url\":\"http://foo:234/serviceconvergence\"}"); + final HttpResponse hostMissingHttpResponse = checker.nodeConvergenceCheck(application, "notPresent:1337", URI.create("http://foo:234/serviceconvergence")); + assertThat(hostMissingHttpResponse.getStatus(), is(410)); + assertJsonResponseEquals(hostMissingHttpResponse, "{\"debug\":{" + + "\"problem\":\"Host:port (service) no longer part of application, refetch list of services.\"," + + "\"wantedGeneration\":3," + + "\"host\":\"notPresent:1337\"}," + + "\"url\":\"http://foo:234/serviceconvergence\"}"); + } + + // When config server constantly redeploys applications we might end up with a higher version than expected, which is OK + @Test + public void convergeGenerationIsLargerThanExpected() throws IOException, SAXException { + ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":4}}")); + checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1))); + } + + private JsonNode string2json(String data) { + try { + return mapper.readTree(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void convergeFailure() throws IOException { + ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{\"generation\":2}}")); + try { + checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1))); + fail("Converge should fail due to config generation not being updated"); + } catch (ConfigNotConvergedException e) { + assertThat(e.getMessage(), is("Timed out waiting for service to use config generation 3 (checking http://localhost:1337/state/v1/config), generation was 2.")); + } + final HttpResponse nodeHttpResponse = checker.nodeConvergenceCheck(application, "localhost:1337", URI.create("http://foo:234/serviceconvergence")); + assertThat(nodeHttpResponse.getStatus(), is(200)); + assertJsonResponseEquals(nodeHttpResponse, "{" + + "\"converged\":false," + + "\"debug\":{\"wantedGeneration\":3,\"currentGeneration\":2,\"host\":\"localhost:1337\" }," + + "\"url\":\"http://foo:234/serviceconvergence\"}"); + } + + @Test + public void stateApiFailure() throws IOException { + ApplicationConvergenceChecker checker = new ApplicationConvergenceChecker((client, serviceUri) -> () -> string2json("{\"config\":{}}")); + try { + checker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(1))); + fail("Converge should fail due to config generation not being updated"); + } catch (ConfigNotConvergedException e) { + assertThat(e.getMessage(), is("Timed out waiting for service to use config generation 3 (checking http://localhost:1337/state/v1/config), could not connect.")); + } + } + + private static class MockModel implements Model { + private final int statePort; + public MockModel(int statePort) { + this.statePort = statePort; + } + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition targetDef, ConfigPayload override) throws IOException { + if (configKey.equals(new ConfigKey<>(ModelConfig.class, ""))) { + return createModelConfig(); + } + throw new UnsupportedOperationException(); + } + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, InnerCNode targetDef, ConfigPayload override) throws IOException { + return getConfig(configKey, (ConfigDefinition)null, override); + } + + private ConfigPayload createModelConfig() { + ModelConfig.Builder builder = new ModelConfig.Builder(); + ModelConfig.Hosts.Builder hostBuilder = new ModelConfig.Hosts.Builder(); + hostBuilder.name("localhost"); + ModelConfig.Hosts.Services.Builder serviceBuilder = new ModelConfig.Hosts.Services.Builder(); + serviceBuilder.type("container"); + serviceBuilder.ports(new ModelConfig.Hosts.Services.Ports.Builder().number(statePort).tags("state")); + hostBuilder.services(serviceBuilder); + builder.hosts(hostBuilder); + ModelConfig config = new ModelConfig(builder); + return ConfigPayload.fromInstance(config); + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced() { + throw new UnsupportedOperationException(); + } + + @Override + public Collection<HostInfo> getHosts() { + throw new UnsupportedOperationException(); + } + + @Override + public Set<String> allConfigIds() { + throw new UnsupportedOperationException(); + } + + @Override + public void distributeFiles(FileDistribution fileDistribution) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional<ProvisionInfo> getProvisionInfo() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java new file mode 100644 index 00000000000..eea951e5c9c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationRepoTest.java @@ -0,0 +1,191 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.MockReloadHandler; +import com.yahoo.vespa.config.server.TestWithCurator; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.1 + */ +public class ApplicationRepoTest extends TestWithCurator { + + @Test + public void require_that_applications_are_read_from_zookeeper() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim", Utf8.toAsciiBytes(3)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bario:staging:bala:bong", Utf8.toAsciiBytes(5)); + ApplicationRepo repo = createZKAppRepo(); + List<ApplicationId> applications = repo.listApplications(); + assertThat(applications.size(), is(3)); + assertThat(applications.get(0).application().value(), is("bario")); + assertThat(applications.get(1).application().value(), is("bar")); + assertThat(applications.get(2).application().value(), is("foo")); + assertThat(repo.getSessionIdForApplication(applications.get(0)), is(5l)); + assertThat(repo.getSessionIdForApplication(applications.get(1)), is(4l)); + assertThat(repo.getSessionIdForApplication(applications.get(2)), is(3l)); + } + + @Test + public void require_that_legacy_application_ids_are_rewritten() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:default:baz:bim", Utf8.toAsciiBytes(3)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4)); + ApplicationRepo repo = createZKAppRepo(); + List<ApplicationId> applications = repo.listApplications(); + assertThat(applications.size(), is(2)); + assertThat(applications.get(0).application().value(), is("bar")); + assertThat(applications.get(1).application().value(), is("foo")); + assertThat(applications.get(0).instance().value(), is("quux")); + assertThat(applications.get(1).instance().value(), is("bim")); + assertNotNull(curatorFramework.checkExists().forPath("/mytenant:foo:bim")); + assertNotNull(curatorFramework.checkExists().forPath("/mytenant:foo:bim")); + assertThat(repo.getSessionIdForApplication(applications.get(0)), is(4l)); + assertThat(repo.getSessionIdForApplication(applications.get(1)), is(3l)); + } + + @Test + public void require_that_legacy_application_ids_are_ignored() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:default:baz:bim", Utf8.toAsciiBytes(3)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:prod:baz:bim", Utf8.toAsciiBytes(3)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:test:bim:quux", Utf8.toAsciiBytes(4)); + ApplicationRepo repo = createZKAppRepo(); + List<ApplicationId> applications = repo.listApplications(); + assertThat(applications.size(), is(2)); + assertThat(repo.getSessionIdForApplication(applications.get(0)), is(4l)); + assertThat(repo.getSessionIdForApplication(applications.get(1)), is(3l)); + } + + @Test + public void require_that_invalid_entries_are_skipped() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim"); + curatorFramework.create().creatingParentsIfNeeded().forPath("/invalid"); + ApplicationRepo repo = createZKAppRepo(); + List<ApplicationId> applications = repo.listApplications(); + assertThat(applications.size(), is(1)); + assertThat(applications.get(0).application().value(), is("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void require_that_requesting_session_for_unknown_application_throws_exception() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim"); + ApplicationRepo repo = createZKAppRepo(); + repo.getSessionIdForApplication(new ApplicationId.Builder() + .tenant("exist") + .applicationName("tenant").instanceName("here").build()); + } + + @Test(expected = IllegalArgumentException.class) + public void require_that_requesting_session_for_empty_application_throws_exception() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:dev:baz:bim"); + ApplicationRepo repo = createZKAppRepo(); + repo.getSessionIdForApplication(new ApplicationId.Builder() + .tenant("tenant") + .applicationName("foo").instanceName("bim").build()); + } + + @Test + public void require_that_application_ids_can_be_written() throws Exception { + ApplicationRepo repo = createZKAppRepo(); + repo.createPutApplicationTransaction(createAppplicationId("myapp"), 3l).commit(); + String path = "/mytenant:myapp:myinst"; + assertTrue(curatorFramework.checkExists().forPath(path) != null); + assertThat(Utf8.toString(curatorFramework.getData().forPath(path)), is("3")); + repo.createPutApplicationTransaction(createAppplicationId("myapp"), 5l).commit(); + assertTrue(curatorFramework.checkExists().forPath(path) != null); + assertThat(Utf8.toString(curatorFramework.getData().forPath(path)), is("5")); + } + + @Test + public void require_that_application_ids_can_be_deleted() throws Exception { + ApplicationRepo repo = createZKAppRepo(); + ApplicationId id1 = createAppplicationId("myapp"); + ApplicationId id2 = createAppplicationId("myapp2"); + repo.createPutApplicationTransaction(id1, 1).commit(); + repo.createPutApplicationTransaction(id2, 1).commit(); + assertThat(repo.listApplications().size(), is(2)); + repo.deleteApplication(id1); + assertThat(repo.listApplications().size(), is(1)); + repo.deleteApplication(id2); + assertThat(repo.listApplications().size(), is(0)); + repo.deleteApplication(id2); + assertThat(repo.listApplications().size(), is(0)); + } + + @Test + public void require_that_repos_behave_similarly() throws Exception { + ApplicationRepo zkRepo = createZKAppRepo(); + ApplicationRepo memRepo = new MemoryApplicationRepo(); + for (ApplicationRepo repo : Arrays.asList(zkRepo, memRepo)) { + ApplicationId id1 = createAppplicationId("myapp"); + ApplicationId id2 = createAppplicationId("myapp2"); + repo.createPutApplicationTransaction(id1, 4).commit(); + repo.createPutApplicationTransaction(id2, 5).commit(); + List<ApplicationId> lst = repo.listApplications(); + Collections.sort(lst); + assertThat(lst.size(), is(2)); + assertThat(lst.get(0).application(), is(id1.application())); + assertThat(lst.get(1).application(), is(id2.application())); + assertThat(repo.getSessionIdForApplication(id1), is(4l)); + assertThat(repo.getSessionIdForApplication(id2), is(5l)); + repo.createPutApplicationTransaction(id1, 6).commit(); + lst = repo.listApplications(); + Collections.sort(lst); + assertThat(lst.size(), is(2)); + assertThat(lst.get(0).application(), is(id1.application())); + assertThat(lst.get(1).application(), is(id2.application())); + assertThat(repo.getSessionIdForApplication(id1), is(6l)); + assertThat(repo.getSessionIdForApplication(id2), is(5l)); + repo.deleteApplication(id1); + assertThat(repo.listApplications().size(), is(1)); + repo.deleteApplication(id2); + assertThat(repo.listApplications().size(), is(0)); + repo.deleteApplication(id2); + } + } + + @Test + public void require_that_reload_handler_is_called_when_apps_are_removed() throws Exception { + curatorFramework.create().creatingParentsIfNeeded().forPath("/foo:test:baz:bim", Utf8.toAsciiBytes(3)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bar:dev:bim:quux", Utf8.toAsciiBytes(4)); + curatorFramework.create().creatingParentsIfNeeded().forPath("/bario:staging:bala:bong", Utf8.toAsciiBytes(5)); + MockReloadHandler reloadHandler = new MockReloadHandler(); + ApplicationRepo repo = createZKAppRepo(reloadHandler); + assertNull(reloadHandler.lastRemoved); + repo.deleteApplication(new ApplicationId.Builder() + .tenant("mytenant") + .applicationName("bar").instanceName("quux").build()); + long endTime = System.currentTimeMillis() + 60_000; + while (System.currentTimeMillis() < endTime && reloadHandler.lastRemoved == null) { + Thread.sleep(100); + } + assertNotNull(reloadHandler.lastRemoved); + assertThat(reloadHandler.lastRemoved.serializedForm(), is("mytenant:bar:quux")); + } + + private ApplicationRepo createZKAppRepo() { + return createZKAppRepo(new MockReloadHandler()); + } + + private ApplicationRepo createZKAppRepo(MockReloadHandler reloadHandler) { + return ZKApplicationRepo.create(curator, Path.createRoot(), reloadHandler, TenantName.from("mytenant")); + } + + private static ApplicationId createAppplicationId(String name) { + return new ApplicationId.Builder() + .tenant("mytenant") + .applicationName(name).instanceName("myinst").build(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java new file mode 100644 index 00000000000..87313ed5b42 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/ApplicationTest.java @@ -0,0 +1,155 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.cloud.config.ModelConfig; +import com.yahoo.cloud.config.SlobroksConfig; +import com.yahoo.cloud.config.log.LogdConfig; +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.jrt.Request; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.JRTClientConfigRequestV3; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; +import com.yahoo.vespa.config.protocol.Trace; +import com.yahoo.vespa.config.server.ModelStub; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.UnknownConfigDefinitionException; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.model.VespaModel; +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1.14 + */ +public class ApplicationTest { + + @Test + public void testThatApplicationIsInitialized() throws IOException, SAXException { + ApplicationId appId = ApplicationId.from(TenantName.defaultName(), + ApplicationName.from("foobar"), InstanceName.defaultName()); + ServerCache cache = new ServerCache(); + Version vespaVersion = Version.fromIntValues(1, 2, 3); + Application app = new Application(new ModelStub(), cache, 1337, vespaVersion, MetricUpdater.createTestUpdater(), appId); + assertThat(app.getApplicationGeneration(), is(1337l)); + assertNotNull(app.getModel()); + assertThat(app.getCache(), is(cache)); + assertThat(app.getName(), is("foobar")); + assertThat(app.getVespaVersion(), is(vespaVersion)); + assertThat(app.toString(), is("application 'foobar', generation 1337, vespa version 1.2.3")); + } + + private static final String[] emptySchema = new String[0]; + + private Application handler; + + @Before + public void setupHandler() throws IOException, SAXException { + File testApp = new File("src/test/apps/app"); + ServerCache cache = createCacheAndAddContent(); + VespaModel model = new VespaModel(FilesApplicationPackage.fromFile(testApp)); + final ApplicationId applicationId = new ApplicationId.Builder().tenant("foo").applicationName("foo").build(); + handler = new Application(model, cache, 1, Version.fromIntValues(1, 2, 3), + new MetricUpdater(Metrics.createTestMetrics(), Metrics.createDimensions(applicationId)), applicationId); + } + + private static ServerCache createCacheAndAddContent() { + ServerCache cache = new ServerCache(); + + final ConfigDefinitionKey key = new ConfigDefinitionKey(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE); + com.yahoo.vespa.config.buildergen.ConfigDefinition def = getDef(key, SimpletypesConfig.CONFIG_DEF_SCHEMA); + // TODO Why do we have to use empty def md5 here? + cache.addDef(key, def); + + final ConfigDefinitionKey key2 = new ConfigDefinitionKey(SlobroksConfig.CONFIG_DEF_NAME, SlobroksConfig.CONFIG_DEF_NAMESPACE); + com.yahoo.vespa.config.buildergen.ConfigDefinition def2 = getDef(key2, SlobroksConfig.CONFIG_DEF_SCHEMA); + cache.addDef(key2, def2); + + final ConfigDefinitionKey key3 = new ConfigDefinitionKey(LogdConfig.CONFIG_DEF_NAME, LogdConfig.CONFIG_DEF_NAMESPACE); + com.yahoo.vespa.config.buildergen.ConfigDefinition def3 = getDef(key3, LogdConfig.CONFIG_DEF_SCHEMA); + cache.addDef(key3, def3); + + return cache; + } + + private static com.yahoo.vespa.config.buildergen.ConfigDefinition getDef(ConfigDefinitionKey key, String[] schema) { + return new com.yahoo.vespa.config.buildergen.ConfigDefinition(key.getName(), schema); + } + + @Test(expected = UnknownConfigDefinitionException.class) + public void require_that_def_file_must_exist() { + handler.resolveConfig(createRequest("unknown", "namespace", "a", emptySchema)); + } + + @Test + public void require_that_known_config_defs_are_found() throws IOException, SAXException { + handler.resolveConfig(createSimpleConfigRequest(emptySchema)); + } + + @Test + public void require_that_build_config_can_be_resolved() throws IOException, SAXException { + List<String> payload = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA)).getLegacyPayload(); + assertTrue(payload.get(1).contains("host")); + } + + @Test + public void require_that_non_existent_fields_in_schema_is_skipped() throws IOException, SAXException { + // Ask for config without schema and check that we get correct default value back + List<String> payload = handler.resolveConfig(createSimpleConfigRequest(emptySchema)).getLegacyPayload(); + assertThat(payload.get(0), is("boolval false")); + // Ask for config with wrong schema + String[] schema = new String[1]; + schema[0] = "boolval bool default=true"; // changed to be true, original is false + payload = handler.resolveConfig(createRequest(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE, "", schema)).getLegacyPayload(); + assertThat(payload.size(), is(1)); + assertThat(payload.get(0), is("boolval true")); + } + + @Test + public void require_that_configs_are_cached() { + ConfigResponse response = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA)); + assertNotNull(response); + ConfigResponse cached_response = handler.resolveConfig(createRequest(ModelConfig.CONFIG_DEF_NAME, ModelConfig.CONFIG_DEF_NAMESPACE, ModelConfig.CONFIG_DEF_MD5, ModelConfig.CONFIG_DEF_SCHEMA)); + assertNotNull(cached_response); + assertTrue(response == cached_response); + } + + private static GetConfigRequest createRequest(String name, String namespace, String defMd5, String[] schema, String configId) { + Request request = JRTClientConfigRequestV3. + createWithParams(new ConfigKey<>(name, configId, namespace, defMd5, null), DefContent.fromArray(schema), + "fromHost", "", 0, 100, Trace.createDummy(), CompressionType.UNCOMPRESSED, + Optional.empty()).getRequest(); + return JRTServerConfigRequestV3.createFromRequest(request); + } + + private static GetConfigRequest createRequest(String name, String namespace, String defMd5, String[] schema) { + return createRequest(name, namespace, defMd5, schema, "admin/model"); + } + + private static GetConfigRequest createSimpleConfigRequest(String[] schema) { + return createRequest(SimpletypesConfig.CONFIG_DEF_NAME, SimpletypesConfig.CONFIG_DEF_NAMESPACE, SimpletypesConfig.CONFIG_DEF_MD5, schema); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java new file mode 100644 index 00000000000..c725775e467 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/MemoryApplicationRepo.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.transaction.Transaction; +import com.yahoo.vespa.config.server.session.DummyTransaction; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * In memory {@link ApplicationRepo} to be used when testing. + * + * @author lulf + * @since 5.1 + */ +public class MemoryApplicationRepo implements ApplicationRepo { + private final Map<ApplicationId, Long> applications = new LinkedHashMap<>(); + private boolean isOpen = true; + + @Override + public List<ApplicationId> listApplications() { + List<ApplicationId> lst = new ArrayList<>(); + lst.addAll(applications.keySet()); + return lst; + } + + @Override + public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) { + return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> { + applications.put(applicationId, sessionId); + }); + } + + @Override + public long getSessionIdForApplication(ApplicationId id) { + if (applications.containsKey(id)) { + return applications.get(id); + } + return 0; + } + + @Override + public void deleteApplication(ApplicationId id) { + applications.remove(id); + } + + @Override + public void close() { + isOpen = false; + } + + public boolean isOpen() { + return isOpen; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java new file mode 100644 index 00000000000..a710384701f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackageTest.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.cloud.config.ConfigserverConfig; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.15 + */ +public class PermanentApplicationPackageTest { + @Test + public void testNonexistingApplication() { + PermanentApplicationPackage permanentApplicationPackage = new PermanentApplicationPackage(new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory("_no_such_dir"))); + assertFalse(permanentApplicationPackage.applicationPackage().isPresent()); + } + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testExistingApplication() throws IOException { + File tmpDir = folder.newFolder(); + PermanentApplicationPackage permanentApplicationPackage = new PermanentApplicationPackage(new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory(tmpDir.getAbsolutePath()))); + assertTrue(permanentApplicationPackage.applicationPackage().isPresent()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java new file mode 100644 index 00000000000..c99561fe7d2 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsBuilder.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author geirst + * @since 5.44 + */ +public class ConfigChangeActionsBuilder { + + private final List<ConfigChangeAction> actions = new ArrayList<>(); + + private static ServiceInfo createService(String clusterName, String clusterType, String serviceType, String serviceName) { + return new ServiceInfo(serviceName, serviceType, null, + ImmutableMap.of("clustername", clusterName, "clustertype", clusterType), + serviceType + "/" + serviceName, "hostname"); + } + + public ConfigChangeActionsBuilder restart(String message, String clusterName, String clusterType, String serviceType, String serviceName) { + actions.add(new MockRestartAction(message, + Arrays.asList(createService(clusterName, clusterType, serviceType, serviceName)))); + return this; + } + + public ConfigChangeActionsBuilder refeed(String name, boolean allowed, String message, String documentType, String clusterName, String serviceName) { + actions.add(new MockRefeedAction(name, + allowed, + message, + Arrays.asList(createService(clusterName, "myclustertype", "myservicetype", serviceName)), documentType)); + return this; + } + + public ConfigChangeActions build() { + return new ConfigChangeActions(actions); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java new file mode 100644 index 00000000000..84c69fef3a1 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverterTest.java @@ -0,0 +1,135 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static com.yahoo.vespa.config.server.configchange.Utils.*; + +/** + * @author geirst + * @since 5.44 + */ +public class ConfigChangeActionsSlimeConverterTest { + + private static String toJson(ConfigChangeActions actions) throws IOException { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + new ConfigChangeActionsSlimeConverter(actions).toSlime(root); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new JsonFormat(false).encode(outputStream, slime); + return outputStream.toString(); + } + + @Test + public void json_representation_of_empty_actions() throws IOException { + ConfigChangeActions actions = new ConfigChangeActionsBuilder().build(); + assertEquals( "{\n" + + " \"configChangeActions\": {\n" + + " \"restart\": [\n" + + " ],\n" + + " \"refeed\": [\n" + + " ]\n" + + " }\n" + + "}\n", + toJson(actions)); + } + + @Test + public void json_representation_of_restart_actions() throws IOException { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2). + restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2).build(); + assertEquals("{\n" + + " \"configChangeActions\": {\n" + + " \"restart\": [\n" + + " {\n" + + " \"clusterName\": \"foo\",\n" + + " \"clusterType\": \"search\",\n" + + " \"serviceType\": \"searchnode\",\n" + + " \"messages\": [\n" + + " \"change\",\n" + + " \"other change\"\n" + + " ],\n" + + " \"services\": [\n" + + " {\n" + + " \"serviceName\": \"baz\",\n" + + " \"serviceType\": \"searchnode\",\n" + + " \"configId\": \"searchnode/baz\",\n" + + " \"hostName\": \"hostname\"\n" + + " },\n" + + " {\n" + + " \"serviceName\": \"qux\",\n" + + " \"serviceType\": \"searchnode\",\n" + + " \"configId\": \"searchnode/qux\",\n" + + " \"hostName\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"refeed\": [\n" + + " ]\n" + + " }\n" + + "}\n", + toJson(actions)); + } + + @Test + public void json_representation_of_refeed_actions() throws IOException { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + refeed(CHANGE_ID, true, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_TYPE). + refeed(CHANGE_ID_2, false, CHANGE_MSG, DOC_TYPE_2, CLUSTER, SERVICE_TYPE).build(); + assertEquals("{\n" + + " \"configChangeActions\": {\n" + + " \"restart\": [\n" + + " ],\n" + + " \"refeed\": [\n" + + " {\n" + + " \"name\": \"change-id\",\n" + + " \"allowed\": true,\n" + + " \"documentType\": \"music\",\n" + + " \"clusterName\": \"foo\",\n" + + " \"messages\": [\n" + + " \"change\"\n" + + " ],\n" + + " \"services\": [\n" + + " {\n" + + " \"serviceName\": \"searchnode\",\n" + + " \"serviceType\": \"myservicetype\",\n" + + " \"configId\": \"myservicetype/searchnode\",\n" + + " \"hostName\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"name\": \"other-change-id\",\n" + + " \"allowed\": false,\n" + + " \"documentType\": \"book\",\n" + + " \"clusterName\": \"foo\",\n" + + " \"messages\": [\n" + + " \"change\"\n" + + " ],\n" + + " \"services\": [\n" + + " {\n" + + " \"serviceName\": \"searchnode\",\n" + + " \"serviceType\": \"myservicetype\",\n" + + " \"configId\": \"myservicetype/searchnode\",\n" + + " \"hostName\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}\n", + toJson(actions)); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java new file mode 100644 index 00000000000..88be73bedba --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockConfigChangeAction.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.List; + +/** + * @author geirst + * @since 5.44 + */ +public abstract class MockConfigChangeAction implements ConfigChangeAction { + + private final String message; + private final List<ServiceInfo> services; + + protected MockConfigChangeAction(String message, List<ServiceInfo> services) { + this.message = message; + this.services = services; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public List<ServiceInfo> getServices() { + return services; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java new file mode 100644 index 00000000000..9043b90b15f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRefeedAction.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeRefeedAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.List; + +/** + * @author geirst + * @since 5.44 + */ +public class MockRefeedAction extends MockConfigChangeAction implements ConfigChangeRefeedAction { + + private final String name; + private final boolean allowed; + private final String documentType; + + public MockRefeedAction(String name, boolean allowed, String message, List<ServiceInfo> services, String documentType) { + super(message, services); + this.name = name; + this.allowed = allowed; + this.documentType = documentType; + } + + @Override + public String name() { return name; } + + @Override + public boolean allowed() { return allowed; } + + @Override + public String getDocumentType() { return documentType; } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java new file mode 100644 index 00000000000..1d546ae65c0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/MockRestartAction.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeRestartAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.List; + +/** + * @author geirst + * @since 5.44 + */ +public class MockRestartAction extends MockConfigChangeAction implements ConfigChangeRestartAction { + public MockRestartAction(String message, List<ServiceInfo> services) { + super(message, services); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java new file mode 100644 index 00000000000..cf4dda7d090 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatterTest.java @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static com.yahoo.vespa.config.server.configchange.Utils.*; + +/** + * @author geirst + * @since 5.44 + */ +public class RefeedActionsFormatterTest { + + @Test + public void formatting_of_single_action() { + RefeedActions actions = new ConfigChangeActionsBuilder(). + refeed(CHANGE_ID, false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + build().getRefeedActions(); + assertEquals("change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" + + " 1) change\n", + new RefeedActionsFormatter(actions).format()); + } + + @Test + public void formatting_of_multiple_actions() { + RefeedActions actions = new ConfigChangeActionsBuilder(). + refeed(CHANGE_ID, false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed(CHANGE_ID, false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed(CHANGE_ID_2, false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed(CHANGE_ID_2, true, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed(CHANGE_ID, false, CHANGE_MSG_2, DOC_TYPE_2, CLUSTER, SERVICE_NAME). + build().getRefeedActions(); + assertEquals("change-id: Consider removing data and re-feed document type 'book' in cluster 'foo' because:\n" + + " 1) other change\n" + + "change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" + + " 1) change\n" + + " 2) other change\n" + + "other-change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" + + " 1) other change\n" + + "(allowed) other-change-id: Consider removing data and re-feed document type 'music' in cluster 'foo' because:\n" + + " 1) other change\n", + new RefeedActionsFormatter(actions).format()); + } + +}
\ No newline at end of file diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java new file mode 100644 index 00000000000..f1bb48eef21 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RefeedActionsTest.java @@ -0,0 +1,74 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static com.yahoo.vespa.config.server.configchange.Utils.*; + +/** + * @author geirst + * @since 5.44 + */ +public class RefeedActionsTest { + + private String toString(RefeedActions.Entry entry) { + StringBuilder builder = new StringBuilder(); + builder.append(entry.getDocumentType() + "." + entry.getClusterName() + ":"); + builder.append(entry.getServices().stream(). + map(service -> service.getServiceName()). + sorted(). + collect(Collectors.joining(",", "[", "]"))); + builder.append(entry.getMessages().stream(). + collect(Collectors.joining(",", "[", "]"))); + return builder.toString(); + } + + @Test + public void action_with_multiple_reasons() { + List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder(). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed("change-id", false, CHANGE_MSG_2, DOC_TYPE, CLUSTER, SERVICE_NAME). + build().getRefeedActions().getEntries(); + assertThat(entries.size(), is(1)); + assertThat(toString(entries.get(0)), equalTo("music.foo:[baz][change,other change]")); + } + + @Test + public void actions_with_multiple_services() { + List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder(). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME_2). + build().getRefeedActions().getEntries(); + assertThat(entries.size(), is(1)); + assertThat(toString(entries.get(0)), equalTo("music.foo:[baz,qux][change]")); + } + + @Test + public void actions_with_multiple_document_types() { + List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder(). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE_2, CLUSTER, SERVICE_NAME). + build().getRefeedActions().getEntries(); + assertThat(entries.size(), is(2)); + assertThat(toString(entries.get(0)), equalTo("book.foo:[baz][change]")); + assertThat(toString(entries.get(1)), equalTo("music.foo:[baz][change]")); + } + + @Test + public void actions_with_multiple_clusters() { + List<RefeedActions.Entry> entries = new ConfigChangeActionsBuilder(). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER, SERVICE_NAME). + refeed("change-id", false, CHANGE_MSG, DOC_TYPE, CLUSTER_2, SERVICE_NAME). + build().getRefeedActions().getEntries(); + assertThat(entries.size(), is(2)); + assertThat(toString(entries.get(0)), equalTo("music.bar:[baz][change]")); + assertThat(toString(entries.get(1)), equalTo("music.foo:[baz][change]")); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java new file mode 100644 index 00000000000..3363e022034 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatterTest.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import org.junit.Test; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static com.yahoo.vespa.config.server.configchange.Utils.*; + +/** + * @author geirst + * @since 5.44 + */ +public class RestartActionsFormatterTest { + + @Test + public void formatting_of_single_action() { + RestartActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + build().getRestartActions(); + assertThat(new RestartActionsFormatter(actions).format(), + equalTo("In cluster 'foo' of type 'search':\n" + + " Restart services of type 'searchnode' because:\n" + + " 1) change\n")); + } + + @Test + public void formatting_of_multiple_actions() { + RestartActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER_2, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + build().getRestartActions(); + assertThat(new RestartActionsFormatter(actions).format(), + equalTo("In cluster 'bar' of type 'search':\n" + + " Restart services of type 'searchnode' because:\n" + + " 1) change\n" + + "In cluster 'foo' of type 'search':\n" + + " Restart services of type 'searchnode' because:\n" + + " 1) change\n" + + " 2) other change\n")); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java new file mode 100644 index 00000000000..4d866e7e2f6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/RestartActionsTest.java @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static com.yahoo.vespa.config.server.configchange.Utils.*; + +/** + * @author geirst + * @since 5.44 + */ +public class RestartActionsTest { + + private String toString(RestartActions.Entry entry) { + StringBuilder builder = new StringBuilder(); + builder.append(entry.getClusterType() + "." + entry.getClusterName() + "." + entry.getServiceType() + ":"); + builder.append(entry.getServices().stream(). + map(service -> service.getServiceName()). + sorted(). + collect(Collectors.joining(",", "[", "]"))); + builder.append(entry.getMessages().stream(). + collect(Collectors.joining(",", "[", "]"))); + return builder.toString(); + } + + @Test + public void actions_with_multiple_reasons() { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG_2, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).build(); + List<RestartActions.Entry> entries = actions.getRestartActions().getEntries(); + assertThat(entries.size(), is(1)); + assertThat(toString(entries.get(0)), equalTo("search.foo.searchnode:[baz][change,other change]")); + } + + @Test + public void actions_with_same_service_type() { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME_2).build(); + List<RestartActions.Entry> entries = actions.getRestartActions().getEntries(); + assertThat(entries.size(), is(1)); + assertThat(toString(entries.get(0)), equalTo("search.foo.searchnode:[baz,qux][change]")); + } + + @Test + public void actions_with_multiple_service_types() { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE_2, SERVICE_NAME).build(); + List<RestartActions.Entry> entries = actions.getRestartActions().getEntries(); + assertThat(entries.size(), is(2)); + assertThat(toString(entries.get(0)), equalTo("search.foo.distributor:[baz][change]")); + assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]")); + } + + @Test + public void actions_with_multiple_clusters_of_same_type() { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER_2, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME).build(); + List<RestartActions.Entry> entries = actions.getRestartActions().getEntries(); + assertThat(entries.size(), is(2)); + assertThat(toString(entries.get(0)), equalTo("search.bar.searchnode:[baz][change]")); + assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]")); + } + + @Test + public void actions_with_multiple_clusters_of_different_type() { + ConfigChangeActions actions = new ConfigChangeActionsBuilder(). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE, SERVICE_TYPE, SERVICE_NAME). + restart(CHANGE_MSG, CLUSTER, CLUSTER_TYPE_2, SERVICE_TYPE, SERVICE_NAME).build(); + List<RestartActions.Entry> entries = actions.getRestartActions().getEntries(); + assertThat(entries.size(), is(2)); + assertThat(toString(entries.get(0)), equalTo("content.foo.searchnode:[baz][change]")); + assertThat(toString(entries.get(1)), equalTo("search.foo.searchnode:[baz][change]")); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java new file mode 100644 index 00000000000..66772488c60 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configchange/Utils.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +/** + * @author geirst + * @since 5.44 + */ +public class Utils { + + final static String CHANGE_ID = "change-id"; + final static String CHANGE_ID_2 = "other-change-id"; + final static String CHANGE_MSG = "change"; + final static String CHANGE_MSG_2 = "other change"; + final static String DOC_TYPE = "music"; + final static String DOC_TYPE_2 = "book"; + final static String CLUSTER = "foo"; + final static String CLUSTER_2 = "bar"; + final static String CLUSTER_TYPE = "search"; + final static String CLUSTER_TYPE_2 = "content"; + final static String SERVICE_TYPE = "searchnode"; + final static String SERVICE_TYPE_2 = "distributor"; + final static String SERVICE_NAME = "baz"; + final static String SERVICE_NAME_2 = "qux"; +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def new file mode 100644 index 00000000000..983f16ac932 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/a.def @@ -0,0 +1,60 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +storage[].feeder[] string +search[].feeder[] string +storage[].id reference +storage[].id2 reference +accesslog string default="" +asyncfetchocc int default=0 +a int default=0 +b int default=0 +functionmodules[] string restart +c int default=0 +d int default=0 +e int default=0 +kanon double +testref reference +testref2 reference +onlyindef int +model string +f[].b string +f[].a string +f[].c string +f[].f string +f[].h string + +# The name of predefined roles. +config[].role string + +## Reference to the config to be used by the role. +config[].id reference + +## Wether the NC should start the corresponding role using the +## slavewrapper utility application or not. +config[].usewrapper bool default=false + +routingtable[].hop[].name string +routingtable[].hop[].selector string +routingtable[].hop[].recipient[] string +listenport int default=13700 + +speciallog[].name string +speciallog[].type string +speciallog[].filehandler.name string default="THEDEF" +speciallog[].filehandler.pattern string default="THEDEF.%Y%m%d%H%M%S" +speciallog[].filehandler.rotation string default="THEDEF0 60 ..." +speciallog[].cachehandler.name string default="THEDEF" +speciallog[].cachehandler.size int default=1000 + +partialsd string default = "def" +partialsd2 string default = "def2" + +rulebase[].name string +rulebase[].isdefault bool default=false +rulebase[].automata string default="" +rulebase[].rules string + +mode enum { BATCH, REALTIME, INCREMENTAL} default=BATCH +rangecheck1 double default=10 range=[-1.6,54] +rangecheck2 int default=10 range=[1,100] +rangecheck3 long default=10 range=[9,13] diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def new file mode 100644 index 00000000000..706a5c4c4f6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/b.def @@ -0,0 +1,4 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +gaff int default=0 +usercfgwithid int diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def new file mode 100644 index 00000000000..61a5fa045d1 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/c.def @@ -0,0 +1,4 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +foo string +gaz int diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def new file mode 100644 index 00000000000..2173d72635b --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/compositeinclude.def @@ -0,0 +1,6 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +classes[].id int +classes[].name string +classes[].fields[].name string +classes[].fields[].type string diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def new file mode 100644 index 00000000000..69cbeb31342 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/d.def @@ -0,0 +1,4 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +thestring string default="g" +theint int default=6 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def new file mode 100644 index 00000000000..ba11bca16cd --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/e.def @@ -0,0 +1,4 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +# this one will be implicit, no cfg +fo int default=-45 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def new file mode 100644 index 00000000000..699649ffebf --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/recursiveinclude.def @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=4 +rec int +ursive string +national int +teatern int +ilscript[].name string +ilscript[].doctype string +ilscript[].content[] string diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def new file mode 100644 index 00000000000..09b5d6718e6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/configdefs/spooler.def @@ -0,0 +1,16 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=2 + +# Which directory to find spool files in. +directory string default="/home/vespa/var/spool/vespa" + +# If true, move successfully processed files to <directory>/success +keepsuccess bool default=false + +# Trace level on error messages from messagebus +tracelevel int default=5 + +# Which parsers to use and config for each of them. +parsers[].classname string +parsers[].parameters[].key string +parsers[].parameters[].value string diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java new file mode 100644 index 00000000000..fbd4f791f83 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java @@ -0,0 +1,104 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.provision.InMemoryProvisioner; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.Capacity; +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.HostFilter; +import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.ProvisionLogger; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.path.Path; +import com.yahoo.transaction.NestedTransaction; +import com.yahoo.vespa.config.server.TestWithTenant; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.PrepareParams; +import com.yahoo.vespa.config.server.session.SilentDeployLogger; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertTrue; + +/** + * @author bratseth + */ +public class HostedDeployTest extends TestWithTenant { + + private static final Path appPath = Path.createRoot().append("testapp"); + private File testApp = new File("src/test/apps/hosted/"); + private Path tenantPath = appPath; + + @Test + public void testRedeploy() throws InterruptedException, IOException { + ApplicationId id = deployApp(); + + Deployer deployer = new Deployer(tenants, HostProvisionerProvider.withProvisioner(createHostProvisioner()), + new ConfigserverConfig(new ConfigserverConfig.Builder()), curator); + + Optional<com.yahoo.config.provision.Deployment> deployment = deployer.deployFromLocalActive(id, Duration.ofSeconds(60)); + assertTrue(deployment.isPresent()); + deployment.get().prepare(); + deployment.get().activate(); + } + + /** + * Do the initial "deploy" with the existing API-less code as the deploy API doesn't support first deploys yet. + */ + private ApplicationId deployApp() throws InterruptedException, IOException { + LocalSession session = tenant.getSessionFactory().createSession(testApp, "default", new SilentDeployLogger(), new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60))); + ApplicationId id = ApplicationId.from(tenant.getName(), ApplicationName.from("myapp"), InstanceName.defaultName()); + session.prepare(new SilentDeployLogger(), new PrepareParams(new ConfigserverConfig(new ConfigserverConfig.Builder())).applicationId(id), Optional.empty(), tenantPath); + session.createActivateTransaction().commit(); + tenant.getLocalSessionRepo().addSession(session); + return id; + } + + private Provisioner createHostProvisioner() { + return new ProvisionerAdapter(new InMemoryProvisioner(true, "host0", "host1", "host2")); + } + + private static class ProvisionerAdapter implements Provisioner { + + private final HostProvisioner hostProvisioner; + + public ProvisionerAdapter(HostProvisioner hostProvisioner) { + this.hostProvisioner = hostProvisioner; + } + + @Override + public List<HostSpec> prepare(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) { + return hostProvisioner.prepare(cluster, capacity, groups, logger); + } + + @Override + public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) { + // noop + } + + @Override + public void removed(ApplicationId application) { + // noop + } + + @Override + public void restart(ApplicationId application, HostFilter filter) { + // noop + } + + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java new file mode 100644 index 00000000000..0dd01404109 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/MockDeployer.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Deployment; + +import java.time.Duration; +import java.util.Optional; + +/** + * @author lulf + */ +public class MockDeployer implements com.yahoo.config.provision.Deployer { + public ApplicationId lastDeployed; + + @Override + public Optional<Deployment> deployFromLocalActive(ApplicationId application, Duration timeout) { + lastDeployed = application; + return Optional.empty(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java new file mode 100644 index 00000000000..6175e8cf1fc --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/RedeployTest.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.TestWithTenant; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.PrepareParams; +import com.yahoo.vespa.config.server.session.SilentDeployLogger; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests redeploying of an already existing application. + * + * @author bratseth + */ +public class RedeployTest extends TestWithTenant { + + private static final Path appPath = Path.createRoot().append("testapp"); + private File testApp = new File("src/test/apps/app"); + private Path tenantPath = appPath; + + @Test + public void testRedeploy() throws InterruptedException, IOException { + ApplicationId id = deployApp(); + + Deployer deployer = new Deployer(tenants, HostProvisionerProvider.empty(), + new ConfigserverConfig(new ConfigserverConfig.Builder()), curator); + + Optional<com.yahoo.config.provision.Deployment> deployment = deployer.deployFromLocalActive(id, Duration.ofSeconds(60)); + assertTrue(deployment.isPresent()); + long activeSessionIdBefore = tenant.getLocalSessionRepo().getActiveSession(id).getSessionId(); + assertEquals(id, tenant.getLocalSessionRepo().getSession(activeSessionIdBefore).getApplicationId()); + deployment.get().prepare(); + deployment.get().activate(); + long activeSessionIdAfter = tenant.getLocalSessionRepo().getActiveSession(id).getSessionId(); + assertEquals(activeSessionIdAfter, activeSessionIdBefore + 1); + assertEquals(id, tenant.getLocalSessionRepo().getSession(activeSessionIdAfter).getApplicationId()); + } + + /** No deploYMENT is done because there isn't a local active session. */ + @Test + public void testNoRedeploy() { + ApplicationId id = ApplicationId.from(TenantName.from("default"), + ApplicationName.from("default"), + InstanceName.from("default")); + + Deployer deployer = new Deployer(tenants, HostProvisionerProvider.empty(), + new ConfigserverConfig(new ConfigserverConfig.Builder()), curator); + + assertFalse(deployer.deployFromLocalActive(id, Duration.ofSeconds(60)).isPresent()); + } + + /** + * Do the initial "deploy" with the existing API-less code as the deploy API doesn't support first deploys yet. + */ + private ApplicationId deployApp() throws InterruptedException, IOException { + LocalSession session = tenant.getSessionFactory().createSession(testApp, "default", new SilentDeployLogger(), new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60))); + ApplicationId id = ApplicationId.from(tenant.getName(), ApplicationName.from("myapp"), InstanceName.defaultName()); + session.prepare(new SilentDeployLogger(), new PrepareParams(new ConfigserverConfig(new ConfigserverConfig.Builder())).applicationId(id), Optional.empty(), tenantPath); + session.createActivateTransaction().commit(); + tenant.getLocalSessionRepo().addSession(session); + return id; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java new file mode 100644 index 00000000000..03cb5ada8ad --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java @@ -0,0 +1,202 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.config.provision.*; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.TestWithCurator; +import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + + +/** + * Unit tests for ZooKeeperClient. + * + * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a> + */ +public class ZooKeeperClientTest extends TestWithCurator { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private ConfigCurator zk; + private String appPath = "/1"; + + @Before + public void setupZK() throws IOException { + this.zk = ConfigCurator.create(curator); + ZooKeeperClient zkc = new ZooKeeperClient(zk, new BaseDeployLogger(), true, Path.fromString(appPath)); + ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(new File("src/test/apps/zkfeed"), new DeployData("foo", "/bar/baz", "appName", 1345l, 3l, 2l)); + Map<Version, FileRegistry> fileRegistries = createFileRegistries(); + app.writeMetaData(); + zkc.setupZooKeeper(); + zkc.feedZooKeeper(app); + zkc.feedZKFileRegistries(fileRegistries); + } + + private Map<Version, FileRegistry> createFileRegistries() { + FileRegistry a = new MockFileRegistry(); + a.addFile("fileA"); + FileRegistry b = new MockFileRegistry(); + b.addFile("fileB"); + Map<Version, FileRegistry> registryMap = new HashMap<>(); + registryMap.put(Version.fromIntValues(1, 2, 3), a); + registryMap.put(Version.fromIntValues(3, 2, 1), b); + return registryMap; + } + + @Test + public void testInitZooKeeper() throws IOException { + ConfigCurator zk = ConfigCurator.create(new MockCurator()); + BaseDeployLogger logger = new BaseDeployLogger(); + long generation = 1L; + ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, Path.fromString("/1")); + zooKeeperClient.setupZooKeeper(); + String appPath = "/"; + assertThat(zk.getChildren(appPath).size(), is(1)); + assertTrue(zk.exists("/" + String.valueOf(generation))); + String currentAppPath = appPath + String.valueOf(generation); + assertTrue(zk.exists(currentAppPath, ConfigCurator.DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", ""))); + assertThat(zk.getChildren(currentAppPath).size(), is(4)); + } + + @Test + public void testFeedDefFilesToZooKeeper() { + String defsPath = appPath + ConfigCurator.DEFCONFIGS_ZK_SUBPATH; + assertTrue(zk.exists(appPath, ConfigCurator.DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", ""))); + List<String> children = zk.getChildren(defsPath); + assertEquals(defsPath + " children", 2, children.size()); + Collections.sort(children); + assertThat(children.get(0), is("a.b.test2,")); + + assertTrue(zk.exists(appPath, ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH.replaceFirst("/", ""))); + String userDefsPath = appPath + ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH; + children = zk.getChildren(userDefsPath); + assertThat(children.size(), is(2)); + Collections.sort(children); + assertThat(children.get(0), is("a.b.test2,")); + } + + // TODO: Evaluate if we want this or not + @Test + @Ignore + public void testFeedComponentsFileReferencesToZooKeeper() throws IOException { + final String appDir = "src/test/apps/app_sdbundles"; + ConfigCurator zk = ConfigCurator.create(new MockCurator()); + BaseDeployLogger logger = new BaseDeployLogger(); + Path app = Path.fromString("/1"); + ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, app); + zooKeeperClient.setupZooKeeper(); + + String currentAppPath = app.getAbsolute(); + assertTrue(zk.exists(currentAppPath, ConfigCurator.USERAPP_ZK_SUBPATH.replaceFirst("/", ""))); + assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH, "components")); + assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle.jar")); + assertTrue(zk.exists(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle2.jar")); + String data = zk.getData(currentAppPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/components", "testbundle2.jar"); + assertThat(data, is(new File(appDir + "/components/testbundle2.jar").getAbsolutePath())); + } + + @Test + public void testFeedUserDefinedFiles() { + assertEquals(zk.getData(appPath+ ConfigCurator.USERAPP_ZK_SUBPATH + "/files", "foo.json"), "foo : foo\n"); + assertEquals(zk.getData(appPath+ ConfigCurator.USERAPP_ZK_SUBPATH + "/files/sub", "bar.json"), "bar : bar\n"); + } + + @Test + public void testFeedAppMetaDataToZooKeeper() { + assertTrue(zk.exists(appPath, ConfigCurator.META_ZK_PATH)); + ApplicationMetaData metaData = ApplicationMetaData.fromJsonString(zk.getData(appPath, ConfigCurator.META_ZK_PATH)); + assertThat(metaData.getApplicationName(), is("appName")); + assertTrue(metaData.getCheckSum().length() > 0); + assertThat(metaData.getDeployedByUser(), is("foo")); + assertThat(metaData.getDeployPath(), is("/bar/baz")); + assertThat(metaData.getDeployTimestamp(), is(1345l)); + assertThat(metaData.getGeneration(), is(3l)); + assertThat(metaData.getPreviousActiveGeneration(), is(2l)); + } + + @Test + public void testVersionedFileRegistry() { + String fileRegPath = appPath + "/" + ZKApplicationPackage.fileRegistryNode; + assertTrue(zk.exists(fileRegPath)); + assertTrue(zk.exists(fileRegPath + "/1.2.3")); + assertTrue(zk.exists(fileRegPath + "/3.2.1")); + // assertNull("Data at " + fileRegPath, zk.getData(fileRegPath)); Not null any more .. hm + } + + @Test + public void include_dirs_are_written_to_ZK() { + assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/" + "dir1", "default.xml")); + assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/nested/" + "dir2", "chain2.xml")); + assertTrue(zk.exists(appPath + ConfigCurator.USERAPP_ZK_SUBPATH + "/nested/" + "dir2", "chain3.xml")); + } + + @Test + public void search_chain_dir_written_to_ZK() { + assertTrue(zk.exists(appPath().append("search").append("chains").append("dir1").append("default.xml").getAbsolute())); + assertTrue(zk.exists(appPath().append("search").append("chains").append("dir2").append("chain2.xml").getAbsolute())); + assertTrue(zk.exists(appPath().append("search").append("chains").append("dir2").append("chain3.xml").getAbsolute())); + } + + @Test + public void search_definitions_written_to_ZK() { + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("music.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("base.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("video.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("book.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("pc.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("laptop.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("product.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("sock.sd").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("foo.expression").getAbsolute())); + assertTrue(zk.exists(appPath().append(ApplicationPackage.SEARCH_DEFINITIONS_DIR).append("bar.expression").getAbsolute())); + } + + private Path appPath() { + return Path.fromString(appPath).append(ConfigCurator.USERAPP_ZK_SUBPATH); + } + + @Test + public void testWritingHostNamesToZooKeeper() throws IOException { + ConfigCurator zk = ConfigCurator.create(new MockCurator()); + BaseDeployLogger logger = new BaseDeployLogger(); + Path app = Path.fromString("/1"); + ZooKeeperClient zooKeeperClient = new ZooKeeperClient(zk, logger, true, app); + zooKeeperClient.setupZooKeeper(); + zooKeeperClient.feedProvisionInfos(createProvisionInfos()); + Path hostsPath = app.append(ZKApplicationPackage.allocatedHostsNode); + assertTrue(zk.exists(hostsPath.getAbsolute())); + assertEquals(0, zk.getBytes(hostsPath.getAbsolute()).length); // Changed from null + assertTrue(zk.exists(hostsPath.append("1.2.3").getAbsolute())); + assertTrue(zk.exists(hostsPath.append("3.2.1").getAbsolute())); + assertTrue(zk.getBytes(hostsPath.append("1.2.3").getAbsolute()).length > 0); + assertTrue(zk.getBytes(hostsPath.append("3.2.1").getAbsolute()).length > 0); + } + + private Map<Version, ProvisionInfo> createProvisionInfos() { + Map<Version, ProvisionInfo> provisionInfoMap = new HashMap<>(); + ProvisionInfo a = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("host.yahoo.com", Collections.emptyList()))); + ProvisionInfo b = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("host2.yahoo.com", Collections.emptyList()))); + provisionInfoMap.put(Version.fromIntValues(1, 2, 3), a); + provisionInfoMap.put(Version.fromIntValues(3, 2, 1), b); + return provisionInfoMap; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java new file mode 100644 index 00000000000..f69e3aa87fa --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployerTest.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.prelude.semantics.parser.ParseException; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.logging.Level; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author lulf + * @since 5.1 + */ +public class ZooKeeperDeployerTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + private static final String defFile = "test2.def"; + + @Test + public void require_that_deployer_is_initialized() throws IOException, ParseException { + ConfigCurator zkfacade = ConfigCurator.create(new MockCurator()); + File serverdbDir = folder.newFolder("serverdb"); + File defsDir = new File(serverdbDir, "serverdefs"); + try { + IOUtils.createWriter(new File(defsDir, defFile), true); + } catch (IOException e) { + e.printStackTrace(); + fail(); + } + deploy(FilesApplicationPackage.fromFile(new File("src/test/apps/content")), zkfacade, Path.fromString("/1")); + deploy(FilesApplicationPackage.fromFile(new File("src/test/apps/content")), zkfacade, Path.fromString("/2")); + } + + public void deploy(ApplicationPackage applicationPackage, ConfigCurator configCurator, Path appPath) throws IOException { + MockDeployLogger logger = new MockDeployLogger(); + ZooKeeperClient client = new ZooKeeperClient(configCurator, logger, true, appPath); + ZooKeeperDeployer deployer = new ZooKeeperDeployer(client); + + deployer.deploy(applicationPackage, Collections.singletonMap(Version.fromIntValues(1, 0, 0), new MockFileRegistry()), Collections.emptyMap()); + assertTrue(configCurator.exists(appPath.getAbsolute())); + } + + private static class MockDeployLogger implements DeployLogger { + @Override + public void log(Level level, String message) { } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java new file mode 100644 index 00000000000..02626bffa9d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLockTest.java @@ -0,0 +1,110 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.vespa.config.server.TestWithCurator; +import com.yahoo.vespa.curator.recipes.CuratorLockException; +import com.yahoo.vespa.curator.mock.MockCurator; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +/** + * @author lulf + */ +public class FileDistributionLockTest extends TestWithCurator { + + FileDistributionLock lock; + private int value = 0; + + @Before + public void setupLock() { + lock = new FileDistributionLock(curator, "/lock"); + value = 0; + } + + @Test + public void testDistributedLock() throws InterruptedException, TimeoutException, ExecutionException { + ExecutorService executor = Executors.newFixedThreadPool(20); + + List<Future<?>> futureList = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + futureList.add(executor.submit(() -> { + lock.lock(); + value++; + lock.unlock(); + })); + } + + for (Future<?> future : futureList) { + future.get(600, TimeUnit.SECONDS); + } + assertThat(value, is(20)); + } + + @Test + public void testDistributedTryLockFailure() throws InterruptedException { + MockCurator mockCurator = new MockCurator(); + lock = new FileDistributionLock(mockCurator, "/mocklock"); + mockCurator.timeoutOnLock = true; + assertFalse(lock.tryLock(600, TimeUnit.SECONDS)); + mockCurator.timeoutOnLock = false; + // Second time should not be blocking + Thread t = new Thread(() -> { + try { + if (lock.tryLock(6, TimeUnit.SECONDS)) { + value = 1; + lock.unlock(); + } + } catch (InterruptedException e) { + } + }); + assertThat(value, is(0)); + t.start(); + t.join(); + assertThat(value, is(1)); + } + + @Test + public void testDistributedLockExceptionFailure() throws InterruptedException { + MockCurator mockCurator = new MockCurator(); + lock = new FileDistributionLock(mockCurator, "/mocklock"); + mockCurator.throwExceptionOnLock = true; + try { + lock.lock(); + fail("Lock call should not succeed"); + } catch (CuratorLockException e) { + // ignore + } + mockCurator.throwExceptionOnLock = false; + // Second time should not be blocking + Thread t = new Thread(() -> { + try { + lock.lock(); + value = 1; + lock.unlock(); + } catch (Exception e) { + fail("Should not fail"); + } + }); + assertThat(value, is(0)); + t.start(); + t.join(); + assertThat(value, is(1)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testConditionNotSupported() { + lock.newCondition(); + } + + @Test(expected = UnsupportedOperationException.class) + public void testLockInterruptiblyNotSupported() throws InterruptedException { + lock.lockInterruptibly(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java new file mode 100644 index 00000000000..a3ba3b26b50 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/ContentHandlerTestBase.java @@ -0,0 +1,103 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import javax.annotation.Nullable; + +import org.junit.Test; +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.collect.Collections2; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.jdisc.http.HttpRequest; + +public abstract class ContentHandlerTestBase extends SessionHandlerTest { + protected String baseUrl = "http://foo:1337/application/v2/tenant/default/session/1/content/"; + + @Test + public void require_that_content_can_be_retrieved() throws IOException { + assertContent("/test.txt", "foo\n"); + assertContent("/foo/", generateResultArray("foo/bar/", "foo/test1.txt", "foo/test2.txt")); + assertContent("/foo", generateResultArray("foo/")); + assertContent("/foo/test1.txt", "bar\n"); + assertContent("/foo/test2.txt", "baz\n"); + assertContent("/foo/bar/", generateResultArray("foo/bar/test.txt")); + assertContent("/foo/bar", generateResultArray("foo/bar/")); + assertContent("/foo/bar/test.txt", "bim\n"); + assertContent("/foo/?recursive=true", generateResultArray("foo/bar/", "foo/bar/test.txt", "foo/test1.txt", "foo/test2.txt")); + } + + @Test + public void require_that_nonexistant_file_returns_not_found() throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.GET, "/test2.txt"); + assertNotNull(response); + assertThat(response.getStatus(), is(NOT_FOUND)); + } + + @Test + public void require_that_return_property_is_used() throws IOException { + assertContent("/test.txt?return=content", "foo\n"); + } + + @Test + public void require_that_illegal_return_property_fails() { + HttpResponse response = doRequest(HttpRequest.Method.GET, "/test.txt?return=foo"); + assertThat(response.getStatus(), is(BAD_REQUEST)); + } + + @Test + public void require_that_status_can_be_retrieved() throws IOException { + assertStatus("/test.txt?return=status", + "{\"status\":\"new\",\"md5\":\"d3b07384d113edec49eaa6238ad5ff00\",\"name\":\"" + baseUrl + "test.txt\"}"); + assertStatus("/foo/?return=status", + "[{\"status\":\"new\",\"md5\":\"\",\"name\":\"" + baseUrl + "foo/bar\"}," + + "{\"status\":\"new\",\"md5\":\"c157a79031e1c40f85931829bc5fc552\",\"name\":\"" + baseUrl + "foo/test1.txt\"}," + + "{\"status\":\"new\",\"md5\":\"258622b1688250cb619f3c9ccaefb7eb\",\"name\":\"" + baseUrl + "foo/test2.txt\"}]"); + assertStatus("/foo/?return=status&recursive=true", + "[{\"status\":\"new\",\"md5\":\"\",\"name\":\"" + baseUrl + "foo/bar\"}," + + "{\"status\":\"new\",\"md5\":\"579cae6111b269c0129af36a2243b873\",\"name\":\"" + baseUrl + "foo/bar/test.txt\"}," + + "{\"status\":\"new\",\"md5\":\"c157a79031e1c40f85931829bc5fc552\",\"name\":\"" + baseUrl + "foo/test1.txt\"}," + + "{\"status\":\"new\",\"md5\":\"258622b1688250cb619f3c9ccaefb7eb\",\"name\":\"" + baseUrl + "foo/test2.txt\"}]"); + } + + protected void assertContent(String path, String expectedContent) throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.GET, path); + assertNotNull(response); + final String renderedString = SessionHandlerTest.getRenderedString(response); + assertThat(renderedString, response.getStatus(), is(OK)); + assertThat(renderedString, is(expectedContent)); + } + + protected void assertStatus(String path, String expectedContent) throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.GET, path); + assertNotNull(response); + final String renderedString = SessionHandlerTest.getRenderedString(response); + assertThat(renderedString, response.getStatus(), is(OK)); + assertThat(renderedString, is(expectedContent)); + } + + protected abstract HttpResponse doRequest(HttpRequest.Method method, String path); + + private String generateResultArray(String... files) { + Collection<String> output = Collections2.transform(Arrays.asList(files), new Function<String, String>() { + @Override + public String apply(@Nullable String input) { + return "\"" + baseUrl + input + "\""; + } + }); + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(Joiner.on(",").join(output)); + sb.append("]"); + return sb.toString(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java new file mode 100644 index 00000000000..22607e8fc26 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HandlerTest.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +/** + * Base class for handler tests + * + * @author musum + * @since 5.1.14 + */ +public class HandlerTest { + public static void assertHttpStatusCodeErrorCodeAndMessage(HttpResponse response, int statusCode, HttpErrorResponse.errorCodes errorCode, String message) throws IOException { + assertNotNull(response); + String renderedString = SessionHandlerTest.getRenderedString(response); + if (renderedString == null) { + renderedString = "assert failed"; + } + assertThat(renderedString, response.getStatus(), is(statusCode)); + if (errorCode != null) { + assertThat(renderedString, containsString(errorCode.name())); + } + assertThat(renderedString, containsString(message)); + } + + public static void assertHttpStatusCodeAndMessage(HttpResponse response, int statusCode, String message) throws IOException { + assertHttpStatusCodeErrorCodeAndMessage(response, statusCode, null, message); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java new file mode 100644 index 00000000000..8525a152403 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigRequestTest.java @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import java.io.IOException; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.vespa.config.ConfigKey; + +import org.junit.Test; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpConfigRequestTest { + @Test + public void require_that_request_can_be_created() { + final ConfigKey<?> configKey = new ConfigKey<>("foo", "myid", "bar"); + + HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest("http://example.yahoo.com:8080/config/v1/" + + configKey.getNamespace() + "." + configKey.getName() + "/" + configKey.getConfigId(), GET)); + assertThat(request.getConfigKey(), is(configKey)); + assertTrue(request.getDefContent().isEmpty()); + } + + @Test + public void require_namespace_can_have_dots() { + final ConfigKey<?> configKey = new ConfigKey<>("foo", "myid", "bar.baz"); + HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest("http://example.yahoo.com:8080/config/v1/" + + configKey.getNamespace() + "." + configKey.getName() + "/" + configKey.getConfigId(), GET)); + assertEquals(request.getConfigKey().getNamespace(), "bar.baz"); + } + + @Test + public void require_that_request_can_be_created_with_advanced_uri() throws IOException { + HttpConfigRequest.createFromRequestV1(HttpRequest.createTestRequest( + "http://example.yahoo.com:19071/config/v1/vespa.config.cloud.sentinel/host-01.example.yahoo.com", GET)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java new file mode 100644 index 00000000000..b7d41e2835e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpConfigResponseTest.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringReader; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpConfigResponseTest { + @Test + public void require_that_response_is_created_from_config() throws IOException { + final long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + // TODO: Hope to be able to remove this mess soon. + DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n"))); + InnerCNode targetDef = dParser.getTree(); + ConfigResponse configResponse = SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5"); + HttpConfigResponse response = HttpConfigResponse.createFromConfig(configResponse); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}")); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java new file mode 100644 index 00000000000..cd18823ea1c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpErrorResponseTest.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpErrorResponseTest { + @Test + public void testThatHttpErrorResponseIsRenderedAsJson() throws IOException { + HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something"); + assertThat(response.getJdiscResponse().getStatus(), is(BAD_REQUEST)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"Error doing something\"}")); + } + + @Test + public void testThatHttpErrorResponseProvidesCorrectErrorMessage() throws IOException { + HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something"); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Error doing something"); + } + + @Test + public void testThatHttpErrorResponseHasJsonContentType() throws IOException { + HttpErrorResponse response = HttpErrorResponse.badRequest("Error doing something"); + assertThat(response.getContentType(), is("application/json")); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java new file mode 100644 index 00000000000..d1c4c5e5e2e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandlerTest.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.server.MockRequestHandler; +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import com.yahoo.config.provision.ApplicationId; + +import org.junit.Before; +import org.junit.Test; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.Executor; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; +import static com.yahoo.jdisc.http.HttpResponse.Status.*; + + +/** + * @author lulf + * @since 5.1 + */ +public class HttpGetConfigHandlerTest { + private static final String configUri = "http://yahoo.com:8080/config/v1/foo.bar/myid"; + + private MockRequestHandler mockRequestHandler; + private HttpGetConfigHandler handler; + + @Before + public void setUp() { + mockRequestHandler = new MockRequestHandler(); + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("bar", "myid", "foo")); + }} ); + handler = new HttpGetConfigHandler(new Executor() { + @SuppressWarnings("NullableProblems") + @Override + public void execute(Runnable command) { + command.run(); + } + }, mockRequestHandler, AccessLog.voidAccessLog()); + } + + @Test + public void require_that_handler_can_be_created() throws IOException { + // Define config response for mock handler + final long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + InnerCNode targetDef = getInnerCNode(); + mockRequestHandler.responses.put(ApplicationId.defaultId(), SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5")); + HttpResponse response = handler.handle(HttpRequest.createTestRequest(configUri, GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}")); + } + + @Test + public void require_correct_error_response() throws IOException { + final String nonExistingConfigNameUri = "http://yahoo.com:8080/config/v1/nonexisting.config/myid"; + final String nonExistingConfigUri = "http://yahoo.com:8080/config/v1/foo.bar/myid/nonexisting/id"; + final String illegalConfigNameUri = "http://yahoo.com:8080/config/v1/foobar/myid"; + + HttpResponse response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigNameUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: nonexisting.config"); + assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config:")); + response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: myid/nonexisting/id"); + assertEquals(response.getContentType(), "application/json"); + assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config id:")); + response = handler.handle(HttpRequest.createTestRequest(illegalConfigNameUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name."); + } + + @Test + public void require_that_nocache_property_works() throws IOException { + long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + InnerCNode targetDef = getInnerCNode(); + mockRequestHandler.responses.put(ApplicationId.defaultId(), SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5")); + final HttpRequest request = HttpRequest.createTestRequest(configUri, GET, null, Collections.singletonMap("nocache", "true")); + HttpResponse response = handler.handle(request); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}")); + } + + private InnerCNode getInnerCNode() { + // TODO: Hope to be able to remove this mess soon. + DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n"))); + return dParser.getTree(); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java new file mode 100644 index 00000000000..7868909f65f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpHandlerTest.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.slime.JsonDecoder; +import com.yahoo.slime.Slime; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.34 + */ +public class HttpHandlerTest { + @Test + public void testResponse() throws IOException { + final String message = "failed"; + HttpHandler httpHandler = new HttpTestHandler(Executors.newSingleThreadExecutor(), AccessLog.voidAccessLog(), new InvalidApplicationException(message)); + HttpResponse response = httpHandler.handle(HttpRequest.createTestRequest("foo", com.yahoo.jdisc.http.HttpRequest.Method.GET)); + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.render(baos); + Slime data = new Slime(); + new JsonDecoder().decode(data, baos.toByteArray()); + assertThat(data.get().field("error-code").asString(), is(HttpErrorResponse.errorCodes.INVALID_APPLICATION_PACKAGE.name())); + assertThat(data.get().field("message").asString(), is(message)); + } + + private static class HttpTestHandler extends HttpHandler { + private RuntimeException exception; + public HttpTestHandler(Executor executor, AccessLog accessLog, RuntimeException exception) { + super(executor, accessLog); + this.exception = exception; + } + + @Override + public HttpResponse handleGET(HttpRequest request) { + throw exception; + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java new file mode 100644 index 00000000000..ad917c5db6d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandlerTest.java @@ -0,0 +1,115 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.MockRequestHandler; +import com.yahoo.vespa.config.server.http.HttpListConfigsHandler.ListConfigsResponse; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.Executor; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpListConfigsHandlerTest { + + private MockRequestHandler mockRequestHandler; + private HttpListConfigsHandler handler; + private HttpListNamedConfigsHandler namedHandler; + + @Before + public void setUp() { + mockRequestHandler = new MockRequestHandler(); + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("bar", "conf/id/", "foo")); + }} ); + handler = new HttpListConfigsHandler(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, AccessLog.voidAccessLog(), mockRequestHandler); + namedHandler = new HttpListNamedConfigsHandler(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, mockRequestHandler, AccessLog.voidAccessLog()); + } + + @Test + public void require_that_handler_can_be_created() throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest("/config/v1/", GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_that_named_handler_can_be_created() throws IOException { + HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.bar/conf/id/", GET); + req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar")); + HttpResponse response = namedHandler.handle(req); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_child_listings_correct() { + Set<ConfigKey<?>> keys = new LinkedHashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("name1", "id/1", "ns1")); + add(new ConfigKey<>("name1", "id/1", "ns1")); + add(new ConfigKey<>("name1", "id/2", "ns1")); + add(new ConfigKey<>("name1", "", "ns1")); + add(new ConfigKey<>("name1", "id/1/1", "ns1")); + add(new ConfigKey<>("name1", "id2", "ns1")); + add(new ConfigKey<>("name1", "id/2/1", "ns1")); + add(new ConfigKey<>("name1", "id/2/1/5/6", "ns1")); + }}; + Set<ConfigKey<?>> keysThatHaveChild = HttpListConfigsHandler.ListConfigsResponse.keysThatHaveAChildWithSameName(keys, keys); + assertEquals(keysThatHaveChild.size(), 3); + } + + @Test + public void require_url_building_and_mimetype_correct() { + HttpListConfigsHandler.ListConfigsResponse resp = new ListConfigsResponse(new HashSet<ConfigKey<?>>(), null, "http://foo.com/config/v1/", true); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), true), "http://foo.com/config/v1/mynamespace.myconfig/my/id"); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), false), "http://foo.com/config/v1/mynamespace.myconfig/my/id/"); + assertEquals(resp.getContentType(), "application/json"); + + } + + @Test + public void require_error_on_bad_request() throws IOException { + HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foobar/conf/id/", GET); + HttpResponse resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name."); + req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.barNOPE/conf/id/", GET); + resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: foo.barNOPE"); + req = HttpRequest.createTestRequest("http://foo.com:8080/config/v1/foo.bar/conf/id/NOPE/", GET); + resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: conf/id/NOPE/"); + } + + @Test + public void require_correct_error_response_on_no_model() throws IOException { + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>()); + HttpResponse response = namedHandler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v1/foo.bar/myid/", GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, + HttpErrorResponse.errorCodes.NOT_FOUND, + "Config not available, verify that an application package has been deployed and activated."); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java new file mode 100644 index 00000000000..2e5576869b5 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerTestBase.java @@ -0,0 +1,266 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import static com.yahoo.jdisc.Response.Status.*; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; + +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.model.application.provider.MockFileRegistry; +import com.yahoo.config.provision.*; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.SuperModelGenerationCounter; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.deploy.ZooKeeperClient; +import com.yahoo.vespa.config.server.session.*; + +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.model.VespaModelFactory; +import org.hamcrest.core.Is; +import org.junit.Test; + +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.config.model.application.provider.DeployData; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.vespa.config.server.HostRegistry; +import com.yahoo.vespa.config.server.PathProvider; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +public abstract class SessionActiveHandlerTestBase extends SessionHandlerTest { + + private File testApp = new File("src/test/apps/app"); + protected static final String appName = "default"; + protected TenantName tenant = null; + protected ConfigCurator configCurator; + protected Curator curator; + protected RemoteSessionRepo remoteSessionRepo; + protected LocalSessionRepo localRepo; + protected PathProvider pathProvider; + protected ApplicationRepo applicationRepo; + protected String activatedMessage = " activated."; + protected String tenantMessage = ""; + + @Test + public void testThatPreviousSessionIsDeactivated() throws Exception { + RemoteSession firstSession = activateAndAssertOK(90l, 0l); + activateAndAssertOK(91l, 90l); + assertThat(firstSession.getStatus(), is(Session.Status.DEACTIVATE)); + } + + @Test + public void testForceActivationWithActivationInBetween() throws Exception { + activateAndAssertOK(90l, 0l); + activateAndAssertOK(92l, 89l, "?force=true"); + } + + @Test + public void testUnknownSession() throws Exception { + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, 9999L, "?timeout=1.0")); + assertEquals(response.getStatus(), 404); + } + + @Test + public void testActivationWithActivationInBetween() throws Exception { + activateAndAssertOK(90l, 0l); + activateAndAssertError(92l, 89l, + HttpErrorResponse.errorCodes.BAD_REQUEST, + getActivateLogPre() + + "Cannot activate session 92 because the currently active session (90) has changed since session 92 was created (was 89 at creation time)"); + } + + protected abstract String getActivateLogPre(); + + @Test + public void testActivationOfUnpreparedSession() throws Exception { + // Needed so we can test that previous active session is still active after a failed activation + RemoteSession firstSession = activateAndAssertOK(90l, 0l); + long sessionId = 91l; + ActivateRequest activateRequest = new ActivateRequest(sessionId, 0l, Session.Status.NEW, "").invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + RemoteSession session = activateRequest.getSession(); + assertThat(actResponse.getStatus(), is(BAD_REQUEST)); + assertThat(getRenderedString(actResponse), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"" + getActivateLogPre() + "Session " + sessionId + " is not prepared\"}")); + assertThat(session.getStatus(), is(not(Session.Status.ACTIVATE))); + assertThat(firstSession.getStatus(), is(Session.Status.ACTIVATE)); + } + + @Test + public void testActivationWithBarrierTimeout() throws Exception { + // Needed so we can test that previous active session is still active after a failed activation + activateAndAssertOK(90l, 0l); + ((MockCurator) curator).timeoutBarrierOnEnter(true); + ActivateRequest activateRequest = new ActivateRequest(91l, 90l, "").invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + assertThat(actResponse.getStatus(), is(INTERNAL_SERVER_ERROR)); + } + + @Test + public void testActivationOfSessionThatDoesNotExistAsLocalSession() throws Exception { + ActivateRequest activateRequest = new ActivateRequest(90l, 0l, "").invoke(false); + HttpResponse actResponse = activateRequest.getActResponse(); + assertThat(actResponse.getStatus(), is(NOT_FOUND)); + String message = getRenderedString(actResponse); + assertThat(message, is("{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 90 was not found\"}")); + } + + @Test + public void require_that_session_created_from_active_that_is_no_longer_active_cannot_be_activated() throws Exception { + long sessionId = 1; + activateAndAssertOK(1, 0); + sessionId++; + activateAndAssertOK(sessionId, 1); + + sessionId++; + ActivateRequest activateRequest = new ActivateRequest(sessionId, 1, "").invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + String message = getRenderedString(actResponse); + assertThat(message, actResponse.getStatus(), Is.is(BAD_REQUEST)); + assertThat(message, + containsString("Cannot activate session 3 because the currently active session (2) has changed since session 3 was created (was 1 at creation time)")); + } + + @Test + public void testAlreadyActivatedSession() throws Exception { + activateAndAssertOK(1, 0); + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, 1l)); + String message = getRenderedString(response); + assertThat(message, response.getStatus(), Is.is(BAD_REQUEST)); + assertThat(message, containsString("Session 1 is already active")); + } + + protected abstract SessionHandler createHandler() throws Exception; + + private RemoteSession createRemoteSession(long sessionId, Session.Status status, SessionZooKeeperClient zkClient) throws IOException { + zkClient.writeStatus(status); + ZooKeeperClient zkC = new ZooKeeperClient(configCurator, new BaseDeployLogger(), false, pathProvider.getSessionDirs().append(String.valueOf(sessionId))); + VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry()); + zkC.feedZKFileRegistries(Collections.singletonMap(modelFactory.getVersion(), new MockFileRegistry())); + zkC.feedProvisionInfos(Collections.singletonMap(modelFactory.getVersion(), ProvisionInfo.withHosts(Collections.emptySet()))); + RemoteSession session = new RemoteSession(TenantName.from("default"), sessionId, new TestComponentRegistry(curator, configCurator, new ModelFactoryRegistry(Collections.singletonList(modelFactory))), zkClient); + remoteSessionRepo.addSession(session); + return session; + } + + private LocalSessionRepo addLocalSession(long sessionId, DeployData deployData, SessionZooKeeperClient zkc) { + writeApplicationId(zkc, deployData.getApplicationName()); + TenantFileSystemDirs tenantFileSystemDirs = TenantFileSystemDirs.createTestDirs(tenant); + ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(testApp, + deployData + ); + localRepo.addSession(new LocalSession(tenant, sessionId, new SessionTest.MockSessionPreparer(), new SessionContext(app, zkc, new File(tenantFileSystemDirs.path(), String.valueOf(sessionId)), applicationRepo, new HostRegistry<>(), new SuperModelGenerationCounter(curator)))); + return localRepo; + } + + protected abstract void writeApplicationId(SessionZooKeeperClient zkc, String applicationName); + + protected abstract Session activateAndAssertOK(long sessionId, long previousSessionId, String subPath) throws Exception; + + protected abstract RemoteSession activateAndAssertOK(long sessionId, long previousSessionId) throws Exception; + + protected ActivateRequest activateAndAssertOKPut(long sessionId, long previousSessionId, String subPath) throws Exception { + ActivateRequest activateRequest = new ActivateRequest(sessionId, previousSessionId, subPath).invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + String message = getRenderedString(actResponse); + assertThat(message, actResponse.getStatus(), is(OK)); + assertActivationMessageOK(activateRequest, message); + RemoteSession session = activateRequest.getSession(); + assertThat(session.getStatus(), is(Session.Status.ACTIVATE)); + return activateRequest; + } + + protected abstract void assertActivationMessageOK(ActivateRequest activateRequest, String message) throws IOException; + + protected abstract void activateAndAssertError(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception; + + protected ActivateRequest activateAndAssertErrorPut(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception { + ActivateRequest activateRequest = new ActivateRequest(sessionId, previousSessionId, "").invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + RemoteSession session = activateRequest.getSession(); + assertThat(actResponse.getStatus(), is(BAD_REQUEST)); + String message = getRenderedString(actResponse); + assertThat(message, is("{\"error-code\":\"" + errorCode.name() + "\",\"message\":\"" + expectedError + "\"}")); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + return activateRequest; + } + + protected void testUnsupportedMethod(com.yahoo.container.jdisc.HttpRequest request) throws Exception { + HttpResponse response = createHandler().handle(request); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED, + HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, + "Method '" + request.getMethod().name() + "' is not supported"); + } + + protected class ActivateRequest { + private long sessionId; + private RemoteSession session; + private SessionHandler handler; + private HttpResponse actResponse; + private Session.Status initialStatus; + private DeployData deployData; + private ApplicationMetaData metaData; + private String subPath; + + public ActivateRequest(long sessionId, long previousSessionId, String subPath) { + this(sessionId, previousSessionId, Session.Status.PREPARE, subPath); + } + + public ActivateRequest(long sessionId, long previousSessionId, Session.Status initialStatus, String subPath) { + this.sessionId = sessionId; + this.initialStatus = initialStatus; + this.deployData = new DeployData("foo", "bar", appName, 0l, sessionId, previousSessionId); + this.subPath = subPath; + } + + public RemoteSession getSession() { + return session; + } + + public SessionHandler getHandler() { + return handler; + } + + public HttpResponse getActResponse() { + return actResponse; + } + + public long getSessionId() { + return sessionId; + } + + public ApplicationMetaData getMetaData() { + return metaData; + } + + public ActivateRequest invoke() throws Exception { + return invoke(true); + } + + public ActivateRequest invoke(boolean createLocalSession) throws Exception { + SessionZooKeeperClient zkClient = new MockSessionZKClient(curator, pathProvider.getSessionDirs().append(String.valueOf(sessionId)), Optional.of(ProvisionInfo.withHosts(Collections.singleton(new HostSpec("bar", Collections.emptyList()))))); + session = createRemoteSession(sessionId, initialStatus, zkClient); + if (createLocalSession) { + LocalSessionRepo repo = addLocalSession(sessionId, deployData, zkClient); + metaData = repo.getSession(sessionId).getMetaData(); + } + handler = createHandler(); + actResponse = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.ACTIVE, sessionId, subPath)); + return this; + } + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java new file mode 100644 index 00000000000..43fdb2d747a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionContentHandlerTestBase.java @@ -0,0 +1,126 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.common.io.Files; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.text.Utf8; +import org.apache.commons.io.FileUtils; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public abstract class SessionContentHandlerTestBase extends ContentHandlerTestBase { + + @Test + public void require_that_directories_can_be_created() throws IOException { + assertMkdir("/bar/"); + assertMkdir("/bar/brask/"); + assertMkdir("/bar/brask/"); + assertMkdir("/bar/brask/bram/"); + assertMkdir("/brask/og/bram/"); + }// TODO: Enable when we have a predictable way of checking request body existence. + + @Test + @Ignore + public void require_that_mkdir_with_body_is_illegal() throws IOException { + HttpResponse response = put("/foobio/", "foo"); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST)); + } + + @Test + public void require_that_nonexistant_session_returns_not_found() throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.GET, "/test.txt", 2l); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.NOT_FOUND)); + } + + protected HttpResponse put(String path, String content) { + ByteArrayInputStream data = new ByteArrayInputStream(Utf8.toBytes(content)); + return doRequest(HttpRequest.Method.PUT, path, data); + } + + @Test + public void require_that_file_write_without_body_is_illegal() throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.PUT, "/foobio.txt"); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST)); + } + + @Test + public void require_that_files_can_be_written() throws IOException { + assertWriteFile("/foo/minfil.txt", "Mycontent"); + assertWriteFile("/foo/minfil.txt", "Differentcontent"); + } + + @Test + public void require_that_nonexistant_file_returs_not_found_when_deleted() throws IOException { + assertDeleteFile(Response.Status.NOT_FOUND, "/test2.txt", "{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 1 does not contain a file 'test2.txt'\"}"); + } + + @Test + public void require_that_files_can_be_deleted() throws IOException { + assertDeleteFile(Response.Status.OK, "/test.txt"); + assertDeleteFile(Response.Status.NOT_FOUND, "/test.txt", "{\"error-code\":\"NOT_FOUND\",\"message\":\"Session 1 does not contain a file 'test.txt'\"}"); + assertDeleteFile(Response.Status.BAD_REQUEST, "/newtest", "{\"error-code\":\"BAD_REQUEST\",\"message\":\"File 'newtest' is not an empty directory\"}"); + assertDeleteFile(Response.Status.OK, "/newtest/testfile.txt"); + assertDeleteFile(Response.Status.OK, "/newtest"); + } + + @Test + public void require_that_status_is_given_for_new_files() throws IOException { + assertStatus("/test.txt?return=status", + "{\"status\":\"new\",\"md5\":\"d3b07384d113edec49eaa6238ad5ff00\",\"name\":\"http://foo:1337" + pathPrefix + "1/content/test.txt\"}"); + assertWriteFile("/test.txt", "Mycontent"); + assertStatus("/test.txt?return=status", + "{\"status\":\"changed\",\"md5\":\"01eabd73c69d78d0009ec93cd62d7f77\",\"name\":\"http://foo:1337" + pathPrefix + "1/content/test.txt\"}"); + } + + private void assertWriteFile(String path, String content) throws IOException { + HttpResponse response = put(path, content); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.OK)); + assertContent(path, content); + assertThat(SessionHandlerTest.getRenderedString(response), + is("{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}")); + } + + private void assertDeleteFile(int statusCode, String filePath) throws IOException { + assertDeleteFile(statusCode, filePath, "{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}"); + } + + private void assertDeleteFile(int statusCode, String filePath, String expectedResponse) throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.DELETE, filePath); + assertNotNull(response); + assertThat(response.getStatus(), is(statusCode)); + assertThat(SessionHandlerTest.getRenderedString(response), is(expectedResponse)); + } + + private void assertMkdir(String path) throws IOException { + HttpResponse response = doRequest(HttpRequest.Method.PUT, path); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.OK)); + assertThat(SessionHandlerTest.getRenderedString(response), + is("{\"prepared\":\"http://foo:1337" + pathPrefix + "1/prepared\"}")); + } + + protected File createTestApp() throws IOException { + File testApp = Files.createTempDir(); + FileUtils.copyDirectory(new File("src/test/apps/content"), testApp); + return testApp; + } + + protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId); + protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, InputStream data); + protected abstract HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId, InputStream data); +}
\ No newline at end of file diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java new file mode 100644 index 00000000000..6a6a4097319 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionCreateHandlerTestBase.java @@ -0,0 +1,216 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.common.io.Files; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.io.IOUtils; +import com.yahoo.vespa.config.server.CompressedApplicationInputStreamTest; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.SessionFactory; + +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static com.yahoo.jdisc.Response.Status.*; +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Tests for session create handlers, to make it easier to have + * similar tests for more than one version of the API. + * + * @author musum + * @since 5.1.28 + */ +public abstract class SessionCreateHandlerTestBase extends SessionHandlerTest { + + public static final HashMap<String, String> postHeaders = new HashMap<>(); + + protected String pathPrefix = "/application/v2/session/"; + protected String createdMessage = " created.\""; + protected String tenantMessage = ""; + + public File testApp = new File("src/test/apps/app"); + public LocalSessionRepo localSessionRepo; + public ApplicationRepo applicationRepo; + + static { + postHeaders.put(SessionCreate.contentTypeHeader, SessionCreate.APPLICATION_X_GZIP); + } + + @Ignore + @Test + public void require_that_from_parameter_cannot_be_set_if_data_in_request() throws IOException { + HttpRequest request = post(Collections.singletonMap("from", "active")); + HttpResponse response = createHandler().handle(request); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Parameter 'from' is illegal for POST"); + } + + @Test + public void require_that_post_request_must_contain_data() throws IOException { + HttpResponse response = createHandler().handle(post()); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Request contains no data"); + } + + @Test + public void require_that_post_request_must_have_correct_content_type() throws IOException { + HashMap<String, String> headers = new HashMap<>(); // no Content-Type header + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + HttpResponse response = createHandler().handle(post(outFile, headers, null)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Request contains no Content-Type header"); + } + + @Test + public void require_that_application_name_is_given_from_parameter() throws IOException { + Map<String, String> params = Collections.singletonMap("name", "ulfio"); + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + MockSessionFactory factory = new MockSessionFactory(); + createHandler(factory).handle(post(outFile, postHeaders, params)); + assertTrue(factory.createCalled); + assertThat(factory.applicationName, is("ulfio")); + } + + protected void assertFromParameter(String expected, String from) throws IOException { + HttpRequest request = post(Collections.singletonMap("from", from)); + MockSessionFactory factory = new MockSessionFactory(); + factory.applicationPackage = testApp; + HttpResponse response = createHandler(factory).handle(request); + assertNotNull(response); + assertThat(response.getStatus(), is(OK)); + assertTrue(factory.createFromCalled); + assertThat(SessionHandlerTest.getRenderedString(response), + is("{\"log\":[]" + tenantMessage + ",\"session-id\":\"" + expected + "\",\"prepared\":\"http://" + hostname + ":" + port + pathPrefix + + expected + "/prepared\",\"content\":\"http://" + hostname + ":" + port + pathPrefix + + expected + "/content/\",\"message\":\"Session " + expected + createdMessage + "}")); + } + + protected void assertIllegalFromParameter(String fromValue) throws IOException { + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + HttpRequest request = post(outFile, postHeaders, Collections.singletonMap("from", fromValue)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(createHandler().handle(request), BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Parameter 'from' has illegal value '" + fromValue + "'"); + } + + @Test + public void require_that_prepare_url_is_returned_on_success() throws IOException { + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + Map<String, String> parameters = Collections.singletonMap("name", "foo"); + HttpResponse response = createHandler().handle(post(outFile, postHeaders, parameters)); + assertNotNull(response); + assertThat(response.getStatus(), is(OK)); + assertThat(SessionHandlerTest.getRenderedString(response), + is("{\"log\":[]" + tenantMessage + ",\"session-id\":\"0\",\"prepared\":\"http://" + + hostname + ":" + port + pathPrefix + "0/prepared\",\"content\":\"http://" + + hostname + ":" + port + pathPrefix + "0/content/\",\"message\":\"Session 0" + createdMessage + "}")); + } + + @Test + public void require_that_session_factory_is_called() throws IOException { + MockSessionFactory sessionFactory = new MockSessionFactory(); + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + createHandler(sessionFactory).handle(post(outFile)); + assertTrue(sessionFactory.createCalled); + } + + @Test + public void require_that_handler_does_not_support_get() throws IOException { + HttpResponse response = createHandler().handle(HttpRequest.createTestRequest(pathPrefix, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED, + HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, + "Method 'GET' is not supported"); + } + + @Test + public void require_internal_error_when_exception() throws IOException { + MockSessionFactory factory = new MockSessionFactory(); + factory.doThrow = true; + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + HttpResponse response = createHandler(factory).handle(post(outFile)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, INTERNAL_SERVER_ERROR, + HttpErrorResponse.errorCodes.INTERNAL_SERVER_ERROR, + "foo"); + } + + @Test + public void require_that_handler_unpacks_application() throws IOException { + MockSessionFactory sessionFactory = new MockSessionFactory(); + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + createHandler(sessionFactory).handle(post(outFile)); + assertTrue(sessionFactory.createCalled); + final File applicationPackage = sessionFactory.applicationPackage; + assertNotNull(applicationPackage); + assertTrue(applicationPackage.exists()); + final File[] files = applicationPackage.listFiles(); + assertNotNull(files); + assertThat(files.length, is(2)); + } + + @Test + public void require_that_session_is_stored_in_repo() throws IOException { + File outFile = CompressedApplicationInputStreamTest.createTarFile(); + createHandler(new MockSessionFactory()).handle(post(outFile)); + assertNotNull(localSessionRepo.getSession(0l)); + } + + public abstract SessionHandler createHandler(); + + public abstract SessionHandler createHandler(SessionFactory sessionFactory); + + public abstract HttpRequest post() throws FileNotFoundException; + + public abstract HttpRequest post(File file) throws FileNotFoundException; + + public abstract HttpRequest post(File file, Map<String, String> headers, Map<String, String> parameters) throws FileNotFoundException; + + public abstract HttpRequest post(Map<String, String> parameters) throws FileNotFoundException; + + public static class MockSessionFactory implements SessionFactory { + public boolean createCalled = false; + public boolean createFromCalled = false; + public boolean doThrow = false; + public File applicationPackage; + public String applicationName; + + @Override + public LocalSession createSession(File applicationDirectory, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget) { + createCalled = true; + this.applicationName = applicationName; + if (doThrow) { + throw new RuntimeException("foo"); + } + final File tempDir = Files.createTempDir(); + try { + IOUtils.copyDirectory(applicationDirectory, tempDir); + } catch (IOException e) { + e.printStackTrace(); + } + this.applicationPackage = tempDir; + return new SessionHandlerTest.MockSession(0, FilesApplicationPackage.fromFile(applicationPackage)); + } + + @Override + public LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, TimeoutBudget timeoutBudget) { + if (doThrow) { + throw new RuntimeException("foo"); + } + createFromCalled = true; + return new SessionHandlerTest.MockSession(existingSession.getSessionId() + 1, FilesApplicationPackage.fromFile(applicationPackage)); + } + } +} + diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java new file mode 100644 index 00000000000..2d7e293fb3c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionExampleHandlerTest.java @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.ThreadedHttpRequestHandler; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; +import org.junit.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; +import static com.yahoo.jdisc.http.HttpRequest.Method.*; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author musum + * @since 5.1.14 + */ +public class SessionExampleHandlerTest { + private static final String URI = "http://localhost:19071/session/example"; + + @Test + public void basicPut() throws IOException { + final SessionExampleHandler handler = new SessionExampleHandler(Executors.newCachedThreadPool()); + final HttpRequest request = HttpRequest.createTestRequest(URI, PUT); + HttpResponse response = handler.handle(request); + assertThat(response.getStatus(), is(OK)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"test\":\"PUT received\"}")); + } + + @Test + public void invalidMethod() { + final SessionExampleHandler handler = new SessionExampleHandler(Executors.newCachedThreadPool()); + final HttpRequest request = HttpRequest.createTestRequest(URI, GET); + HttpResponse response = handler.handle(request); + assertThat(response.getStatus(), is(METHOD_NOT_ALLOWED)); + } + + + /** + * A handler that prepares a session given by an id in the request. + * + * @author musum + * @since 5.1.14 + */ + public static class SessionExampleHandler extends ThreadedHttpRequestHandler { + + public SessionExampleHandler(Executor executor) { + super(executor); + } + + @Override + public HttpResponse handle(HttpRequest request) { + final com.yahoo.jdisc.http.HttpRequest.Method method = request.getMethod(); + switch (method) { + case PUT: + return handlePUT(request); + case GET: + return new SessionExampleResponse(METHOD_NOT_ALLOWED, "Method '" + method + "' is not supported"); + default: + return new SessionExampleResponse(INTERNAL_SERVER_ERROR); + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + HttpResponse handlePUT(HttpRequest request) { + return new SessionExampleResponse(OK, "PUT received"); + } + + private static class SessionExampleResponse extends HttpResponse { + private final Slime slime = new Slime(); + private final Cursor root = slime.setObject(); + private final String message; + + + private SessionExampleResponse(int status) { + this(status, ""); + headers().put("Cache-Control","max-age=120"); + } + + private SessionExampleResponse(int status, String message) { + super(status); + this.message = message; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + root.setString("test", message); + new JsonFormat(true).encode(outputStream, slime); + } + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java new file mode 100644 index 00000000000..d38b7f9e586 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionHandlerTest.java @@ -0,0 +1,164 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.transaction.Transaction; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.HostRegistry; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.config.server.session.*; + +import java.io.*; +import java.util.Optional; + +/** + * Base class for session handler tests + * + * @author musum + * @since 5.1.14 + */ +public class SessionHandlerTest { + + protected String pathPrefix = "/application/v2/session/"; + public static final String hostname = "foo"; + public static final int port = 1337; + + public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id, String subPath, InputStream data) { + return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path + "/" + id + "/" + cmd.toString() + subPath, method, data); + } + + public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id, String subPath) { + return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path + "/" + id + "/" + cmd.toString() + subPath, method); + } + + public static HttpRequest createTestRequest(String path, com.yahoo.jdisc.http.HttpRequest.Method method, Cmd cmd, Long id) { + return createTestRequest(path, method, cmd, id, ""); + } + + public static HttpRequest createTestRequest(String path) { + return HttpRequest.createTestRequest("http://" + hostname + ":" + port + path, com.yahoo.jdisc.http.HttpRequest.Method.PUT); + } + + public static String getRenderedString(HttpResponse response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.render(baos); + return baos.toString("UTF-8"); + } + + public static class MockSession extends LocalSession { + + public boolean doVerboseLogging = false; + public Session.Status status; + private final SessionPreparer preparer; + private final ApplicationPackage app; + private ConfigChangeActions actions = new ConfigChangeActions(); + private long createTime = System.currentTimeMillis() / 1000; + private ApplicationId applicationId; + + public MockSession(long id, ApplicationPackage app) { + super(TenantName.defaultName(), id, null, new SessionContext(null, new MockSessionZKClient(MockApplicationPackage.createEmpty()), null, null, new HostRegistry<>(), null)); + this.app = app; + this.preparer = new SessionTest.MockSessionPreparer(); + } + + public MockSession(long sessionId, ApplicationPackage applicationPackage, long createTime) { + this(sessionId, applicationPackage); + this.createTime = createTime; + } + + public MockSession(long sessionId, ApplicationPackage applicationPackage, ConfigChangeActions actions) { + this(sessionId, applicationPackage); + this.actions = actions; + } + + public MockSession(long sessionId, ApplicationPackage app, ApplicationId applicationId) { + this(sessionId, app); + this.applicationId = applicationId; + } + + @Override + public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> application, Path tenantPath) { + status = Session.Status.PREPARE; + if (doVerboseLogging) { + logger.log(LogLevel.DEBUG, "debuglog"); + } + return actions; + } + + public void setStatus(Session.Status status) { + this.status = status; + } + + @Override + public Session.Status getStatus() { + return this.status; + } + + @Override + public Transaction createDeactivateTransaction() { + return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> { + status = Status.DEACTIVATE; + }); + } + + @Override + public Transaction createActivateTransaction() { + return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> { + status = Status.ACTIVATE; + }); + } + + @Override + public ApplicationFile getApplicationFile(Path relativePath, Mode mode) { + if (mode == Mode.WRITE) { + status = Status.NEW; + } + if (preparer == null) { + return null; + } + ApplicationPackage pkg = app; + if (pkg == null) { + return null; + } + return pkg.getFile(relativePath); + } + + @Override + public ApplicationId getApplicationId() { + return applicationId; + } + + @Override + public long getCreateTime() { + return createTime; + } + + @Override + public void delete() { } + } + + public static enum Cmd { + PREPARED("prepared"), + ACTIVE("active"), + CONTENT("content"); + private final String name; + + private Cmd(String s) { + this.name = s; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java new file mode 100644 index 00000000000..885d4164196 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/SessionPrepareHandlerTestBase.java @@ -0,0 +1,185 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.session.*; + +import com.yahoo.vespa.curator.Curator; +import org.junit.Test; + +import java.io.IOException; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.jdisc.http.HttpResponse.Status.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author musum + * @since 5.1.14 + */ +public abstract class SessionPrepareHandlerTestBase extends SessionHandlerTest { + + protected Curator curator; + private SessionZooKeeperClient zooKeeperClient; + protected LocalSessionRepo localRepo; + + protected String preparedMessage = " prepared.\"}"; + protected String tenantMessage = ""; + + + @Test + public void require_error_when_session_id_does_not_exist() throws Exception { + // No session with this id exists + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 9999L)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "Session 9999 was not found"); + } + + @Test + public void require_error_when_session_id_not_a_number() throws Exception { + final String session = "notanumber/prepared"; + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix + session)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, + HttpErrorResponse.errorCodes.BAD_REQUEST, + "Session id in request is not a number, request was 'http://" + hostname + ":" + port + pathPrefix + session + "'"); + } + + @Test + public void require_that_handler_gives_error_for_unsupported_methods() throws Exception { + testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, Method.POST, Cmd.PREPARED, 1L)); + testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, Method.DELETE, Cmd.PREPARED, 1L)); + } + + protected void testUnsupportedMethod(HttpRequest request) throws Exception { + HttpResponse response = createHandler().handle(request); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, METHOD_NOT_ALLOWED, + HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, + "Method '" + request.getMethod().name() + "' is not supported"); + } + + @Test + public void require_that_activate_url_is_returned_on_success() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L)); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + assertNotNull(response); + assertThat(response.getStatus(), is(OK)); + assertResponseContains(response, "\"activate\":\"http://foo:1337" + pathPrefix + "1/active\",\"message\":\"Session 1" + preparedMessage); + } + + @Test + public void require_debug() throws Exception { + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 9999L, "?debug=true")); + assertThat(response.getStatus(), is(NOT_FOUND)); + assertThat(SessionHandlerTest.getRenderedString(response), containsString("NotFoundException")); + } + + @Test + public void require_verbose() throws Exception { + MockSession session = new MockSession(1, null); + session.doVerboseLogging = true; + localRepo.addSession(session); + HttpResponse response = createHandler().handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L, "?verbose=true")); + assertThat(response.getStatus(), is(OK)); + assertThat(SessionHandlerTest.getRenderedString(response), containsString("debuglog")); + } + + /** + * A mock remote session repo based on contents of local repo + */ + private RemoteSessionRepo fromLocalSessionRepo(LocalSessionRepo localRepo) { + RemoteSessionRepo remoteRepo = new RemoteSessionRepo(); + PathProvider pathProvider = new PathProvider(Path.createRoot()); + for (LocalSession ls : localRepo.listSessions()) { + + zooKeeperClient = new MockSessionZKClient(curator, pathProvider.getSessionDirs().append(String.valueOf(ls.getSessionId()))); + if (ls.getStatus()!=null) zooKeeperClient.writeStatus(ls.getStatus()); + RemoteSession remSess = new RemoteSession(TenantName.from("default"), ls.getSessionId(), + new TestComponentRegistry(), + zooKeeperClient); + remoteRepo.addSession(remSess); + } + return remoteRepo; + } + + @Test + public void require_get_response_activate_url_on_ok() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo)); + sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L)); + session.setStatus(Session.Status.PREPARE); + zooKeeperClient.writeStatus(Session.Status.PREPARE); + HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L)); + assertResponseContains(getResponse, "\"activate\":\"http://foo:1337" + pathPrefix + "1/active\",\"message\":\"Session 1" + preparedMessage); + } + + @Test + public void require_get_response_error_on_not_prepared() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo)); + session.setStatus(Session.Status.NEW); + zooKeeperClient.writeStatus(Session.Status.NEW); + HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, BAD_REQUEST, + HttpErrorResponse.errorCodes.BAD_REQUEST, + "Session not prepared: 1"); + session.setStatus(Session.Status.ACTIVATE); + zooKeeperClient.writeStatus(Session.Status.ACTIVATE); + getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 1L)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, BAD_REQUEST, + HttpErrorResponse.errorCodes.BAD_REQUEST, + "Session is active: 1"); + } + + @Test + public void require_cannot_prepare_active_session() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + session.setStatus(Session.Status.ACTIVATE); + SessionHandler sessionHandler = createHandler(fromLocalSessionRepo(localRepo)); + HttpResponse putResponse = sessionHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.PUT, Cmd.PREPARED, 1L)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(putResponse, BAD_REQUEST, + HttpErrorResponse.errorCodes.BAD_REQUEST, + "Session is active: 1"); + } + + @Test + public void require_get_response_error_when_session_id_does_not_exist() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + SessionHandler sessHandler = createHandler(fromLocalSessionRepo(localRepo)); + HttpResponse getResponse = sessHandler.handle(SessionHandlerTest.createTestRequest(pathPrefix, Method.GET, Cmd.PREPARED, 9999L)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(getResponse, NOT_FOUND, + HttpErrorResponse.errorCodes.NOT_FOUND, + "Session 9999 was not found"); + } + + protected static void assertResponse(HttpResponse response, String activateString) throws IOException { + // TODO Test when more logging is added + //assertThat(baos.toString(), startsWith("{\"log\":[{\"time\":")); + assertThat(SessionHandlerTest.getRenderedString(response), endsWith(activateString)); + } + + protected static void assertResponseContains(HttpResponse response, String string) throws IOException { + assertThat(SessionHandlerTest.getRenderedString(response), containsString(string)); + } + + protected static void assertResponseNotContains(HttpResponse response, String string) throws IOException { + assertThat(SessionHandlerTest.getRenderedString(response), not(containsString(string))); + } + + public abstract SessionHandler createHandler() throws Exception; + + public abstract SessionHandler createHandler(RemoteSessionRepo remoteSessionRepo) throws Exception ; +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java new file mode 100644 index 00000000000..1e1bf1a644c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentHandlerTest.java @@ -0,0 +1,111 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.ContentHandlerTestBase; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.Session; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class ApplicationContentHandlerTest extends ContentHandlerTestBase { + + private ApplicationHandler handler; + private TenantName tenant1 = TenantName.from("mofet"); + private TenantName tenant2 = TenantName.from("bla"); + private String baseServer = "http://foo:1337"; + + private ApplicationId idTenant1 = new ApplicationId.Builder() + .tenant(tenant1) + .applicationName("foo").instanceName("quux").build(); + private ApplicationId idTenant2 = new ApplicationId.Builder() + .tenant(tenant2) + .applicationName("foo").instanceName("quux").build(); + private MockSession session2; + + @Before + public void setupHandler() throws Exception { + TestTenantBuilder testTenantBuilder = new TestTenantBuilder(); + testTenantBuilder.createTenant(tenant1); + testTenantBuilder.createTenant(tenant2); + session2 = new MockSession(2l, FilesApplicationPackage.fromFile(new File("src/test/apps/content"))); + testTenantBuilder.tenants().get(tenant1).getLocalSessionRepo().addSession(session2); + testTenantBuilder.tenants().get(tenant2).getLocalSessionRepo().addSession(new MockSession(3l, FilesApplicationPackage.fromFile(new File("src/test/apps/content2")))); + testTenantBuilder.tenants().get(tenant1).getApplicationRepo().createPutApplicationTransaction(idTenant1, 2l).commit(); + testTenantBuilder.tenants().get(tenant2).getApplicationRepo().createPutApplicationTransaction(idTenant2, 3l).commit(); + handler = new ApplicationHandler(command -> command.run(), AccessLog.voidAccessLog(), testTenantBuilder.createTenants(), HostProvisionerProvider.empty(), Zone.defaultZone(), null, null); + pathPrefix = createPath(idTenant1, Zone.defaultZone()); + baseUrl = baseServer + pathPrefix; + } + + private String createPath(ApplicationId applicationId, Zone zone) { + return "/application/v2/tenant/" + + applicationId.tenant().value() + + "/application/" + + applicationId.application().value() + + "/environment/" + + zone.environment().value() + + "/region/" + + zone.region().value() + + "/instance/" + + applicationId.instance().value() + + "/content/"; + } + + @Test + public void require_that_nonexistant_application_returns_not_found() throws IOException { + assertNotFound(HttpRequest.createTestRequest(baseServer + createPath(new ApplicationId.Builder() + .tenant("tenant") + .applicationName("notexist").instanceName("baz").build(), Zone.defaultZone()), + com.yahoo.jdisc.http.HttpRequest.Method.GET)); + assertNotFound(HttpRequest.createTestRequest(baseServer + createPath(new ApplicationId.Builder() + .tenant("unknown") + .applicationName("notexist").instanceName("baz").build(), Zone.defaultZone()), + com.yahoo.jdisc.http.HttpRequest.Method.GET)); + } + + @Test + public void require_that_multiple_tenants_are_handled() throws IOException { + assertContent("/test.txt", "foo\n"); + pathPrefix = createPath(idTenant2, Zone.defaultZone()); + baseUrl = baseServer + pathPrefix; + assertContent("/test.txt", "bar\n"); + } + + @Test + public void require_that_get_does_not_set_write_flag() throws IOException { + session2.status = Session.Status.PREPARE; + assertContent("/test.txt", "foo\n"); + assertThat(session2.status, is(Session.Status.PREPARE)); + } + + private void assertNotFound(HttpRequest request) { + HttpResponse response = handler.handle(request); + assertNotNull(response); + assertThat(response.getStatus(), is(Response.Status.NOT_FOUND)); + } + + @Override + protected HttpResponse doRequest(com.yahoo.jdisc.http.HttpRequest.Method method, String path) { + HttpRequest request = HttpRequest.createTestRequest(baseUrl + path, method); + return handler.handle(request); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java new file mode 100644 index 00000000000..1eb38902e3f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java @@ -0,0 +1,289 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.vespa.config.server.MockReloadHandler; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker; +import com.yahoo.vespa.config.server.application.LogServerLogGrabber; +import com.yahoo.vespa.config.server.http.HandlerTest; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.MockSessionZKClient; +import com.yahoo.vespa.config.server.session.RemoteSession; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.model.VespaModelFactory; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author musum + * @since 5.4 + */ +public class ApplicationHandlerTest { + + private static File testApp = new File("src/test/apps/app"); + + private ApplicationHandler handler; + private ListApplicationsHandler listApplicationsHandler; + private final static TenantName mytenant = TenantName.from("mytenant"); + private final static TenantName foobar = TenantName.from("foobar"); + private Tenants tenants; + private SessionActiveHandlerTest.MockProvisioner provisioner; + private MockStateApiFactory stateApiFactory = new MockStateApiFactory(); + + @Before + public void setup() throws Exception { + TestTenantBuilder testBuilder = new TestTenantBuilder(); + testBuilder.createTenant(mytenant).withReloadHandler(new MockReloadHandler()); + testBuilder.createTenant(foobar).withReloadHandler(new MockReloadHandler()); + + tenants = testBuilder.createTenants(); + provisioner = new SessionActiveHandlerTest.MockProvisioner(); + handler = createApplicationHandler( + provisioner, new ApplicationConvergenceChecker(stateApiFactory), new LogServerLogGrabber()); + listApplicationsHandler = new ListApplicationsHandler( + Runnable::run, AccessLog.voidAccessLog(), tenants, Zone.defaultZone()); + } + + private ApplicationHandler createApplicationHandler( + Provisioner provisioner, + ApplicationConvergenceChecker convergeChecker, + LogServerLogGrabber logServerLogGrabber) { + return new ApplicationHandler( + Runnable::run, + AccessLog.voidAccessLog(), + tenants, + HostProvisionerProvider.withProvisioner(provisioner), + Zone.defaultZone(), + convergeChecker, + logServerLogGrabber); + } + + @Test + public void testDelete() throws Exception { + ApplicationId defaultId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build(); + assertApplicationExists(mytenant, null, Zone.defaultZone()); + + long sessionId = 1; + addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId); + assertApplicationExists(mytenant, defaultId, Zone.defaultZone()); + assertFalse(provisioner.removed); + deleteAndAssertOKResponse(defaultId); + assertTrue(provisioner.removed); + assertThat(provisioner.lastApplicationId.tenant(), is(mytenant)); + assertThat(provisioner.lastApplicationId, is(defaultId)); + sessionId++; + addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId); + deleteAndAssertOKResponse(defaultId, true); + + ApplicationId fooId = new ApplicationId.Builder() + .tenant(mytenant) + .applicationName("foo").instanceName("quux").build(); + sessionId++; + addApplication(tenants.tenantsCopy().get(mytenant), fooId, sessionId); + addApplication(tenants.tenantsCopy().get(foobar), fooId, sessionId); + assertApplicationExists(mytenant, fooId, Zone.defaultZone()); + assertApplicationExists(foobar, fooId, Zone.defaultZone()); + deleteAndAssertOKResponse(fooId, true); + assertThat(provisioner.lastApplicationId.tenant(), is(mytenant)); + assertThat(provisioner.lastApplicationId, is(fooId)); + assertApplicationExists(mytenant, null, Zone.defaultZone()); + assertApplicationExists(foobar, fooId, Zone.defaultZone()); + + + sessionId++; + ApplicationId baliId = new ApplicationId.Builder() + .tenant(mytenant) + .applicationName("bali").instanceName("quux").build(); + addApplication(tenants.tenantsCopy().get(mytenant), baliId, sessionId); + deleteAndAssertOKResponse(baliId, true); + assertApplicationExists(mytenant, null, Zone.defaultZone()); + } + + @Test + public void testGet() throws Exception { + long sessionId = 1; + ApplicationId defaultId = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build(); + addApplication(tenants.tenantsCopy().get(mytenant), defaultId, sessionId); + assertApplicationGeneration(defaultId, Zone.defaultZone(), 1, true); + assertApplicationGeneration(defaultId, Zone.defaultZone(), 1, false); + } + + @Test + public void testRestart() throws Exception { + long sessionId = 1; + ApplicationId application = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build(); + addApplication(tenants.tenantsCopy().get(mytenant), application, sessionId); + assertFalse(provisioner.restarted); + restart(application, Zone.defaultZone()); + assertTrue(provisioner.restarted); + assertEquals(application, provisioner.lastApplicationId); + } + + @Test + public void testConverge() throws Exception { + long sessionId = 1; + ApplicationId application = new ApplicationId.Builder().applicationName(ApplicationName.defaultName()).tenant(mytenant).build(); + addApplication(tenants.tenantsCopy().get(mytenant), application, sessionId); + assertFalse(stateApiFactory.createdApi); + converge(application, Zone.defaultZone()); + assertTrue(stateApiFactory.createdApi); + } + + @Test + public void testPutIsIllegal() throws IOException { + assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.PUT); + } + + @Test + @Ignore + public void testFailingProvisioner() throws Exception { + provisioner = new SessionActiveHandlerTest.FailingMockProvisioner(); + handler = createApplicationHandler( + provisioner, new ApplicationConvergenceChecker(stateApiFactory), new LogServerLogGrabber()); + final ApplicationId applicationId = ApplicationId.defaultId(); + addApplication(tenants.tenantsCopy().get(mytenant), applicationId, 1); + assertApplicationExists(mytenant, applicationId, Zone.defaultZone()); + provisioner.activated = true; + + String url = "http://myhost:14000/application/v2/tenant/" + mytenant + "/application/" + applicationId.application(); + deleteAndAssertResponse(url, 500, null, "{\"message\":\"Cannot remove application\"}", com.yahoo.jdisc.http.HttpRequest.Method.DELETE); + assertApplicationExists(mytenant, applicationId, Zone.defaultZone()); + Assert.assertTrue(provisioner.activated); + } + + static void addApplication(Tenant tenant, ApplicationId applicationId, long sessionId) throws Exception { + tenant.getApplicationRepo().createPutApplicationTransaction(applicationId, sessionId).commit(); + ApplicationPackage app = FilesApplicationPackage.fromFile(testApp); + addLocalSession(tenant, app, sessionId, applicationId); + addRemoteSession(tenant, app, sessionId); + } + + static void addLocalSession(Tenant tenant, ApplicationPackage app, long sessionId, ApplicationId applicationId) { + LocalSessionRepo localRepo = tenant.getLocalSessionRepo(); + localRepo.addSession(new SessionHandlerTest.MockSession(sessionId, app, applicationId)); + } + + static void addRemoteSession(Tenant tenant, ApplicationPackage app, long sessionId) { + RemoteSessionRepo remoteRepo = tenant.getRemoteSessionRepo(); + remoteRepo.addSession(new RemoteSession(tenant.getName(), sessionId, new TestComponentRegistry(new MockCurator(), new ModelFactoryRegistry(Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())))), new MockSessionZKClient(app))); + } + + private void assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException { + String url = "http://myhost:14000/application/v2/tenant/" + mytenant + "/application/default"; + deleteAndAssertResponse(url, Response.Status.METHOD_NOT_ALLOWED, HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method + "' is not supported\"}", + method); + } + + private void deleteAndAssertOKResponse(ApplicationId applicationId) throws IOException { + deleteAndAssertOKResponse(applicationId, true); + } + + private void deleteAndAssertOKResponse(ApplicationId applicationId, boolean fullAppIdInUrl) throws IOException { + long sessionId = tenants.tenantsCopy().get(applicationId.tenant()).getApplicationRepo().getSessionIdForApplication(applicationId); + deleteAndAssertResponse(applicationId, Zone.defaultZone(), Response.Status.OK, null, fullAppIdInUrl); + assertNull(tenants.tenantsCopy().get(applicationId.tenant()).getLocalSessionRepo().getSession(sessionId)); + } + + private void deleteAndAssertResponse(ApplicationId applicationId, Zone zone, int expectedStatus, HttpErrorResponse.errorCodes errorCode, boolean fullAppIdInUrl) throws IOException { + String expectedResponse = "{\"message\":\"Application '" + applicationId + "' deleted\"}"; + deleteAndAssertResponse(toUrlPath(applicationId, zone, fullAppIdInUrl), expectedStatus, errorCode, expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method.DELETE); + } + + private void assertApplicationGeneration(ApplicationId applicationId, Zone zone, long expectedGeneration, boolean fullAppIdInUrl) throws IOException { + assertApplicationGeneration(toUrlPath(applicationId, zone, fullAppIdInUrl), expectedGeneration); + } + + private String toUrlPath(ApplicationId application, Zone zone, boolean fullAppIdInUrl) { + String url = "http://myhost:14000/application/v2/tenant/" + application.tenant().value() + "/application/" + application.application().value(); + if (fullAppIdInUrl) + url = url + "/environment/" + zone.environment().value() + "/region/" + zone.region().value() + "/instance/" + application.instance().value(); + return url; + } + + private void assertApplicationGeneration(String url, long expectedGeneration) throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET)); + HandlerTest.assertHttpStatusCodeAndMessage(response, 200, "{\"generation\":" + expectedGeneration + "}"); + } + + private void deleteAndAssertResponse(String url, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method)); + if (expectedStatus == 200) { + HandlerTest.assertHttpStatusCodeAndMessage(response, 200, expectedResponse); + } else { + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse); + } + } + + private void assertApplicationExists(TenantName tenantName, ApplicationId applicationId, Zone zone) throws IOException { + String expected = applicationId == null ? "[]" : "[\"http://myhost:14000/application/v2/tenant/" + tenantName + "/application/" + applicationId.application().value() + + "/environment/" + zone.environment().value() + + "/region/" + zone.region().value() + + "/instance/" + applicationId.instance().value() + "\"]"; + ListApplicationsHandlerTest.assertResponse(listApplicationsHandler, "http://myhost:14000/application/v2/tenant/" + tenantName + "/application/", + Response.Status.OK, + expected, + com.yahoo.jdisc.http.HttpRequest.Method.GET); + } + + private void restart(ApplicationId application, Zone zone) throws IOException { + String restartUrl = toUrlPath(application, zone, true) + "/restart"; + HttpResponse response = handler.handle(HttpRequest.createTestRequest(restartUrl, com.yahoo.jdisc.http.HttpRequest.Method.POST)); + HandlerTest.assertHttpStatusCodeAndMessage(response, 200, ""); + } + + private void converge(ApplicationId application, Zone zone) throws IOException { + String restartUrl = toUrlPath(application, zone, true) + "/converge"; + HttpResponse response = handler.handle(HttpRequest.createTestRequest(restartUrl, com.yahoo.jdisc.http.HttpRequest.Method.GET)); + HandlerTest.assertHttpStatusCodeAndMessage(response, 200, ""); + } + + private static class MockStateApiFactory implements ApplicationConvergenceChecker.StateApiFactory { + public boolean createdApi = false; + @Override + public ApplicationConvergenceChecker.StateApi createStateApi(Client client, URI serviceUri) { + createdApi = true; + return () -> { + try { + return new ObjectMapper().readTree("{\"config\":{\"generation\":1}}"); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java new file mode 100644 index 00000000000..50ef9176771 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HostHandlerTest.java @@ -0,0 +1,114 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.*; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.http.HandlerTest; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author musum + * @since 5.4 + */ +public class HostHandlerTest { + private static final String urlPrefix = "http://myhost:14000/application/v2/host/"; + + private HostHandler handler; + private final static TenantName mytenant = TenantName.from("mytenant"); + private final static String hostname = "testhost"; + private Tenants tenants; + private HostRegistries hostRegistries; + private HostHandler hostHandler; + + @Before + public void setup() throws Exception { + TestTenantBuilder testBuilder = new TestTenantBuilder(); + testBuilder.createTenant(mytenant).withReloadHandler(new MockReloadHandler()); + + tenants = testBuilder.createTenants(); + handler = createHostHandler(); + } + + private HostHandler createHostHandler() { + final HostRegistry<TenantName> hostRegistry = new HostRegistry<>(); + hostRegistry.update(mytenant, Collections.singletonList(hostname)); + TestComponentRegistry testComponentRegistry = new TestComponentRegistry(); + hostRegistries = testComponentRegistry.getHostRegistries(); + hostRegistries.createApplicationHostRegistry(mytenant).update(ApplicationId.from(mytenant, ApplicationName.defaultName(), InstanceName.defaultName()), Collections.singletonList(hostname)); + hostRegistries.getTenantHostRegistry().update(mytenant, Collections.singletonList(hostname)); + hostHandler = new HostHandler(command -> { + command.run(); + }, AccessLog.voidAccessLog(), testComponentRegistry); + return hostHandler; + } + + @Test + public void require_correct_tenant_and_application_for_hostname() throws Exception { + assertThat(hostRegistries, is(hostHandler.hostRegistries)); + long sessionId = 1; + ApplicationId id = ApplicationId.from(mytenant, ApplicationName.defaultName(), InstanceName.defaultName()); + ApplicationHandlerTest.addApplication(tenants.tenantsCopy().get(mytenant), id, sessionId); + assertApplicationForHost(hostname, mytenant, id, Zone.defaultZone()); + } + + @Test + public void require_that_handler_gives_error_for_unknown_hostname() throws Exception { + long sessionId = 1; + ApplicationHandlerTest.addApplication(tenants.tenantsCopy().get(mytenant), ApplicationId.defaultId(), sessionId); + final String hostname = "unknown"; + assertErrorForHost(hostname, + Response.Status.NOT_FOUND, + HttpErrorResponse.errorCodes.NOT_FOUND, + "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not find any application using host '" + hostname + "'\"}"); + } + + @Test + public void require_that_only_get_method_is_allowed() throws IOException { + assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.PUT); + assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.POST); + assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method.DELETE); + } + + private void assertNotAllowed(com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException { + String url = urlPrefix + hostname; + deleteAndAssertResponse(url, Response.Status.METHOD_NOT_ALLOWED, + HttpErrorResponse.errorCodes.METHOD_NOT_ALLOWED, + "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method + "' is not supported\"}", + method); + } + + private void assertApplicationForHost(String hostname, TenantName expectedTenantName, ApplicationId expectedApplicationId, Zone zone) throws IOException { + String url = urlPrefix + hostname; + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET)); + HandlerTest.assertHttpStatusCodeAndMessage(response, Response.Status.OK, + "{\"tenant\":\"" + expectedTenantName.value() + "\"," + + "\"application\":\"" + expectedApplicationId.application().value() + "\"," + + "\"environment\":\"" + zone.environment().value() + "\"," + + "\"region\":\"" + zone.region().value() + "\"," + + "\"instance\":\"" + expectedApplicationId.instance().value() + "\"}" + ); + } + + private void assertErrorForHost(String hostname, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse) throws IOException { + String url = urlPrefix + hostname; + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, com.yahoo.jdisc.http.HttpRequest.Method.GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse); + } + + private void deleteAndAssertResponse(String url, int expectedStatus, HttpErrorResponse.errorCodes errorCode, String expectedResponse, com.yahoo.jdisc.http.HttpRequest.Method method) throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, expectedStatus, errorCode, expectedResponse); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java new file mode 100644 index 00000000000..b1a330e5f99 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandlerTest.java @@ -0,0 +1,140 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.HashSet; +import java.util.concurrent.Executor; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import org.junit.Before; +import org.junit.Test; +import com.yahoo.config.SimpletypesConfig; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.text.StringUtilities; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.MockRequestHandler; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.HandlerTest; +import com.yahoo.vespa.config.server.http.HttpConfigRequest; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; + +public class HttpGetConfigHandlerTest { + + private static final TenantName tenant = TenantName.from("mytenant"); + private static final String EXPECTED_RENDERED_STRING = "{\"boolval\":false,\"doubleval\":0.0,\"enumval\":\"VAL1\",\"intval\":0,\"longval\":0,\"stringval\":\"s\"}"; + private static final String configUri = "http://yahoo.com:8080/config/v2/tenant/" + tenant.value() + "/application/myapplication/foo.bar/myid"; + private MockRequestHandler mockRequestHandler; + private HttpGetConfigHandler handler; + + @Before + public void setUp() throws Exception { + mockRequestHandler = new MockRequestHandler(); + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("bar", "myid", "foo")); + }} ); + TestTenantBuilder tb = new TestTenantBuilder(); + tb.createTenant(tenant).withRequestHandler(mockRequestHandler).build(); + Tenants tenants = tb.createTenants(); + handler = new HttpGetConfigHandler(command -> { + command.run(); + }, AccessLog.voidAccessLog(), tenants); + } + + @Test + public void require_that_handler_can_be_created() throws IOException { + // Define config response for mock handler + final long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + InnerCNode targetDef = getInnerCNode(); + mockRequestHandler.responses.put(new ApplicationId.Builder().tenant(tenant).applicationName("myapplication").build(), + SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5")); + HttpResponse response = handler.handle(HttpRequest.createTestRequest(configUri, GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING)); + } + + @Test + public void require_that_handler_can_handle_long_appid_request_with_configid() throws IOException { + String uriLongAppId = "http://yahoo.com:8080/config/v2/tenant/" + tenant.value() + + "/application/myapplication/environment/staging/region/myregion/instance/myinstance/foo.bar/myid"; + final long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + InnerCNode targetDef = getInnerCNode(); + mockRequestHandler.responses.put(new ApplicationId.Builder() + .tenant(tenant) + .applicationName("myapplication").instanceName("myinstance").build(), + SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5")); + HttpResponse response = handler.handle(HttpRequest.createTestRequest(uriLongAppId, GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING)); + } + + @Test + public void require_that_request_gets_correct_fields_with_full_appid() { + String uriLongAppId = "http://yahoo.com:8080/config/v2/tenant/bill/application/sookie/environment/dev/region/bellefleur/instance/sam/foo.bar/myid"; + HttpRequest r = HttpRequest.createTestRequest(uriLongAppId, GET); + HttpConfigRequest req = HttpConfigRequest.createFromRequestV2(r); + assertThat(req.getApplicationId().tenant().value(), is("bill")); + assertThat(req.getApplicationId().application().value(), is("sookie")); + assertThat(req.getApplicationId().instance().value(), is("sam")); + } + + @Test + public void require_that_request_gets_correct_fields_with_short_appid() { + String uriShortAppId = "http://yahoo.com:8080/config/v2/tenant/jason/application/alcide/foo.bar/myid"; + HttpRequest r = HttpRequest.createTestRequest(uriShortAppId, GET); + HttpConfigRequest req = HttpConfigRequest.createFromRequestV2(r); + assertThat(req.getApplicationId().tenant().value(), is("jason")); + assertThat(req.getApplicationId().application().value(), is("alcide")); + assertThat(req.getApplicationId().instance().value(), is("default")); + } + + @Test + public void require_correct_error_response() throws IOException { + final String nonExistingConfigNameUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/nonexisting.config/myid"; + final String nonExistingConfigUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication//foo.bar/myid/nonexisting/id"; + final String illegalConfigNameUri = "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication//foobar/myid"; + + HttpResponse response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigNameUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: nonexisting.config"); + assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config:")); + response = handler.handle(HttpRequest.createTestRequest(nonExistingConfigUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: myid/nonexisting/id"); + assertEquals(response.getContentType(), "application/json"); + assertTrue(SessionHandlerTest.getRenderedString(response).contains("No such config id:")); + response = handler.handle(HttpRequest.createTestRequest(illegalConfigNameUri, GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name."); + } + + @Test + public void require_that_nocache_property_works() throws IOException { + long generation = 1L; + ConfigPayload payload = ConfigPayload.fromInstance(new SimpletypesConfig(new SimpletypesConfig.Builder())); + InnerCNode targetDef = getInnerCNode(); + mockRequestHandler.responses.put(new ApplicationId.Builder().tenant(tenant).applicationName("myapplication").build(), + SlimeConfigResponse.fromConfigPayload(payload, targetDef, generation, "mymd5")); + final HttpRequest request = HttpRequest.createTestRequest(configUri, GET, null, Collections.singletonMap("nocache", "true")); + HttpResponse response = handler.handle(request); + assertThat(SessionHandlerTest.getRenderedString(response), is(EXPECTED_RENDERED_STRING)); + } + + private InnerCNode getInnerCNode() { + // TODO: Hope to be able to remove this mess soon. + DefParser dParser = new DefParser(SimpletypesConfig.getDefName(), new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n"))); + return dParser.getTree(); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java new file mode 100644 index 00000000000..cad7cbd583c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandlerTest.java @@ -0,0 +1,145 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.MockRequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.http.HandlerTest; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler.ListConfigsResponse; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.*; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; +import static com.yahoo.jdisc.http.HttpRequest.Method.GET; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpListConfigsHandlerTest { + + private MockRequestHandler mockRequestHandler; + private HttpListConfigsHandler handler; + private HttpListNamedConfigsHandler namedHandler; + + @Before + public void setUp() throws Exception { + mockRequestHandler = new MockRequestHandler(); + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("bar", "conf/id", "foo")); + }} ); + TestTenantBuilder tb = new TestTenantBuilder(); + tb.createTenant(TenantName.from("mytenant")).withRequestHandler(mockRequestHandler).build(); + Tenants tenants = tb.createTenants(); + handler = new HttpListConfigsHandler(command -> { + command.run(); + }, AccessLog.voidAccessLog(), tenants, Zone.defaultZone()); + namedHandler = new HttpListNamedConfigsHandler(command -> { + command.run(); + }, AccessLog.voidAccessLog(), tenants, Zone.defaultZone()); + } + + @Test + public void require_that_handler_can_be_created() throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/", GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_that_request_can_be_created_from_full_appid() throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest( + "http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/environment/test/region/myregion/instance/myinstance/", GET)); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_that_named_handler_can_be_created() throws IOException { + HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/conf/id/", GET); + req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar")); + HttpResponse response = namedHandler.handle(req); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_that_named_handler_can_be_created_from_full_appid() throws IOException { + HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/environment/prod/region/myregion/instance/myinstance/foo.bar/conf/id/", GET); + req.getJDiscRequest().parameters().put("http.path", Arrays.asList("foo.bar")); + HttpResponse response = namedHandler.handle(req); + assertThat(SessionHandlerTest.getRenderedString(response), is("{\"children\":[],\"configs\":[]}")); + } + + @Test + public void require_child_listings_correct() { + Set<ConfigKey<?>> keys = new LinkedHashSet<ConfigKey<?>>() {{ + add(new ConfigKey<>("name1", "id/1", "ns1")); + add(new ConfigKey<>("name1", "id/1", "ns1")); + add(new ConfigKey<>("name1", "id/2", "ns1")); + add(new ConfigKey<>("name1", "", "ns1")); + add(new ConfigKey<>("name1", "id/1/1", "ns1")); + add(new ConfigKey<>("name1", "id2", "ns1")); + add(new ConfigKey<>("name1", "id/2/1", "ns1")); + add(new ConfigKey<>("name1", "id/2/1/5/6", "ns1")); + }}; + Set<ConfigKey<?>> keysThatHaveChild = ListConfigsResponse.keysThatHaveAChildWithSameName(keys, keys); + assertEquals(keysThatHaveChild.size(), 3); + } + + @Test + public void require_url_building_and_mimetype_correct() { + ListConfigsResponse resp = new ListConfigsResponse(new HashSet<ConfigKey<?>>(), null, "http://foo.com/config/v2/tenant/mytenant/application/mya/", true); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), true), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig/my/id"); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "my/id", "mynamespace"), false), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig/my/id/"); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "", "mynamespace"), false), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig"); + assertEquals(resp.toUrl(new ConfigKey<>("myconfig", "", "mynamespace"), true), "http://foo.com/config/v2/tenant/mytenant/application/mya/mynamespace.myconfig"); + assertEquals(resp.getContentType(), "application/json"); + + } + + @Test + public void require_error_on_bad_request() throws IOException { + HttpRequest req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foobar/conf/id/", GET); + HttpResponse resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, BAD_REQUEST, HttpErrorResponse.errorCodes.BAD_REQUEST, "Illegal config, must be of form namespace.name."); + req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.barNOPE/conf/id/", GET); + resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config: foo.barNOPE"); + req = HttpRequest.createTestRequest("http://foo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/conf/id/NOPE/", GET); + resp = namedHandler.handle(req); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(resp, NOT_FOUND, HttpErrorResponse.errorCodes.NOT_FOUND, "No such config id: conf/id/NOPE"); + } + + @Test + public void require_correct_error_response_on_no_model() throws IOException { + mockRequestHandler.setAllConfigs(new HashSet<ConfigKey<?>>()); + HttpResponse response = namedHandler.handle(HttpRequest.createTestRequest("http://yahoo.com:8080/config/v2/tenant/mytenant/application/myapplication/foo.bar/myid/", GET)); + HandlerTest.assertHttpStatusCodeErrorCodeAndMessage(response, NOT_FOUND, + HttpErrorResponse.errorCodes.NOT_FOUND, + "Config not available, verify that an application package has been deployed and activated."); + } + + @Test + public void require_correct_configid_parent() { + assertEquals(ListConfigsResponse.parentConfigId(null), null); + assertEquals(ListConfigsResponse.parentConfigId("foo"), ""); + assertEquals(ListConfigsResponse.parentConfigId(""), ""); + assertEquals(ListConfigsResponse.parentConfigId("/"), ""); + assertEquals(ListConfigsResponse.parentConfigId("foo/bar"), "foo"); + assertEquals(ListConfigsResponse.parentConfigId("foo/bar/baz"), "foo/bar"); + assertEquals(ListConfigsResponse.parentConfigId("foo/bar/"), "foo/bar"); + + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java new file mode 100644 index 00000000000..2e75d1b02e6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandlerTest.java @@ -0,0 +1,111 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.*; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import org.junit.Test; +import org.junit.Before; + +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import static com.yahoo.jdisc.http.HttpRequest.Method.*; + +/** + * @author lulf + * @since 5.1 + */ +public class ListApplicationsHandlerTest { + private ApplicationRepo applicationRepo, applicationRepo2; + private ListApplicationsHandler handler; + + @Before + public void setup() throws Exception { + TestTenantBuilder testBuilder = new TestTenantBuilder(); + TenantName mytenant = TenantName.from("mytenant"); + TenantName foobar = TenantName.from("foobar"); + testBuilder.createTenant(mytenant); + testBuilder.createTenant(foobar); + applicationRepo = testBuilder.tenants().get(mytenant).getApplicationRepo(); + applicationRepo2 = testBuilder.tenants().get(foobar).getApplicationRepo(); + Tenants tenants = testBuilder.createTenants(); + handler = new ListApplicationsHandler(command -> { + command.run(); + }, AccessLog.voidAccessLog(), tenants, new Zone(Environment.dev, RegionName.from("us-east"))); + } + + @Test + public void require_that_applications_are_listed() throws Exception { + final String url = "http://myhost:14000/application/v2/tenant/mytenant/application/"; + assertResponse(url, Response.Status.OK, + "[]"); + applicationRepo.createPutApplicationTransaction( + new ApplicationId.Builder().tenant("tenant").applicationName("foo").instanceName("quux").build(), + 1).commit(); + assertResponse(url, Response.Status.OK, + "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"]"); + applicationRepo.createPutApplicationTransaction( + new ApplicationId.Builder().tenant("tenant").applicationName("bali").instanceName("quux").build(), + 1).commit(); + assertResponse(url, Response.Status.OK, + "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"," + + "\"" + url + "bali/environment/dev/region/us-east/instance/quux\"]" + ); + } + + @Test + public void require_that_get_is_required() throws IOException { + final String url = "http://myhost:14000/application/v2/tenant/mytenant/application/"; + assertResponse(url, Response.Status.METHOD_NOT_ALLOWED, + createMethodNotAllowedMessage(DELETE), DELETE); + assertResponse(url, Response.Status.METHOD_NOT_ALLOWED, + createMethodNotAllowedMessage(PUT), PUT); + assertResponse(url, Response.Status.METHOD_NOT_ALLOWED, + createMethodNotAllowedMessage(POST), POST); + } + + private static String createMethodNotAllowedMessage(Method method) { + return "{\"error-code\":\"METHOD_NOT_ALLOWED\",\"message\":\"Method '" + method.name() + "' is not supported\"}"; + } + + @Test + public void require_that_listing_works_with_multiple_tenants() throws Exception { + applicationRepo.createPutApplicationTransaction(new ApplicationId.Builder() + .tenant("tenant") + .applicationName("foo").instanceName("quux").build(), 1).commit(); + applicationRepo2.createPutApplicationTransaction(new ApplicationId.Builder() + .tenant("tenant") + .applicationName("quux").instanceName("foo").build(), 1).commit(); + String url = "http://myhost:14000/application/v2/tenant/mytenant/application/"; + assertResponse(url, Response.Status.OK, + "[\"" + url + "foo/environment/dev/region/us-east/instance/quux\"]"); + url = "http://myhost:14000/application/v2/tenant/foobar/application/"; + assertResponse(url, Response.Status.OK, + "[\"" + url + "quux/environment/dev/region/us-east/instance/foo\"]"); + } + + void assertResponse(String url, int expectedStatus, String expectedResponse) throws IOException { + assertResponse(url, expectedStatus, expectedResponse, GET); + } + + private void assertResponse(String url, int expectedStatus, String expectedResponse, Method method) throws IOException { + assertResponse(handler, url, expectedStatus, expectedResponse, method); + } + + static void assertResponse(ListApplicationsHandler handler, String url, int expectedStatus, String expectedResponse, Method method) throws IOException { + HttpResponse response = handler.handle(HttpRequest.createTestRequest(url, method)); + assertNotNull(response); + assertThat(response.getStatus(), is(expectedStatus)); + assertThat(SessionHandlerTest.getRenderedString(response), is(expectedResponse)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java new file mode 100644 index 00000000000..9bcc462035a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponseTest.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +public class ListTenantsResponseTest extends TenantTest { + + private final TenantName a = TenantName.from("a"); + private final TenantName b = TenantName.from("b"); + private final TenantName c = TenantName.from("c"); + + @Test + public void testJsonSerialization() throws Exception { + final Collection<TenantName> tenantNames = Arrays.asList(a, b, c); + final ListTenantsResponse response = new ListTenantsResponse(tenantNames); + assertResponseEquals(response, "{\"tenants\":[\"a\",\"b\",\"c\"]}"); + } + + @Test + public void testJsonSerializationNoTenants() throws Exception { + final Collection<TenantName> tenantNames = Collections.emptyList(); + final ListTenantsResponse response = new ListTenantsResponse(tenantNames); + assertResponseEquals(response, "{\"tenants\":[]}"); + } +}
\ No newline at end of file diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java new file mode 100644 index 00000000000..e4f985b7d9f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ListTenantsTest.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.TestWithTenant; +import org.junit.Test; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Method; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertTrue; + +public class ListTenantsTest extends TenantTest { + + private final TenantName a = TenantName.from("a"); + private final TenantName b = TenantName.from("b"); + private final TenantName c = TenantName.from("c"); + + @Test + public void testListTenants() throws Exception { + tenants.createTenant(a); + tenants.createTenant(b); + tenants.createTenant(c); + + ListTenantsHandler listTenantsHandler = new ListTenantsHandler(testExecutor(), null, tenants); + + ListTenantsResponse response = (ListTenantsResponse) listTenantsHandler.handleGET(HttpRequest.createTestRequest("/blabla", Method.GET)); + final Collection<TenantName> responseTenantNames = response.getTenantNames(); + assertTrue(responseTenantNames.containsAll(Arrays.asList(a, b, c))); + assertContainsSystemTenants(responseTenantNames); + } + + private static void assertContainsSystemTenants(final Collection<TenantName> tenantNames) { + assertTrue(tenantNames.contains(TenantName.defaultName())); + assertTrue(tenantNames.contains(ApplicationId.HOSTED_VESPA_TENANT)); + } + + @Test + public void testEmptyTenants() throws Exception { + ListTenantsHandler listTenantsHandler = new ListTenantsHandler(testExecutor(), null, tenants); + + ListTenantsResponse response = (ListTenantsResponse) listTenantsHandler.handleGET(HttpRequest.createTestRequest("/blabla", Method.GET)); + final Collection<TenantName> responseTenantNames = response.getTenantNames(); + assertContainsSystemTenants(responseTenantNames); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java new file mode 100644 index 00000000000..659a435ca6d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java @@ -0,0 +1,217 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; + +import com.yahoo.config.provision.*; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.slime.JsonFormat; +import com.yahoo.transaction.NestedTransaction; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSession; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; +import com.yahoo.vespa.config.server.session.Session; +import com.yahoo.vespa.config.server.session.SessionZooKeeperClient; + +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.Before; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.http.SessionActiveHandlerTestBase; +import com.yahoo.vespa.config.server.http.SessionHandler; +import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase.MockSessionFactory; + +public class SessionActiveHandlerTest extends SessionActiveHandlerTestBase { + + private MockProvisioner hostProvisioner; + + @Before + public void setup() throws Exception { + tenant = TenantName.from("activatetest"); + remoteSessionRepo = new RemoteSessionRepo(); + applicationRepo = new MemoryApplicationRepo(); + curator = new MockCurator(); + configCurator = ConfigCurator.create(curator); + localRepo = new LocalSessionRepo(applicationRepo); + pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; + tenantMessage = ",\"tenant\":\"" + tenant + "\""; + pathProvider = new PathProvider(Path.createRoot()); + activatedMessage = " for tenant '" + tenant + "' activated."; + hostProvisioner = new MockProvisioner(); + } + + @Test + public void require_correct_response_on_success() throws Exception { + activateAndAssertOK(1, 0); + } + + @Test + public void testActivationWithActivationInBetween() throws Exception { + activateAndAssertOK(90l, 0l); + activateAndAssertError(92l, 89l, + HttpErrorResponse.errorCodes.BAD_REQUEST, + "tenant:"+tenant+" app:default:default Cannot activate session 92 because the currently active session (90) has changed since session 92 was created (was 89 at creation time)"); + } + + + @Test + public void testActivationOfUnpreparedSession() throws Exception { + // Needed so we can test that previous active session is still active after a failed activation + RemoteSession firstSession = activateAndAssertOK(90l, 0l); + long sessionId = 91l; + ActivateRequest activateRequest = new ActivateRequest(sessionId, 0l, Session.Status.NEW, "").invoke(); + HttpResponse actResponse = activateRequest.getActResponse(); + RemoteSession session = activateRequest.getSession(); + assertThat(actResponse.getStatus(), is(Response.Status.BAD_REQUEST)); + assertThat(getRenderedString(actResponse), is("{\"error-code\":\"BAD_REQUEST\",\"message\":\"tenant:"+tenant+" app:default:default Session " + sessionId + " is not prepared\"}")); + assertThat(session.getStatus(), is(not(Session.Status.ACTIVATE))); + assertThat(firstSession.getStatus(), is(Session.Status.ACTIVATE)); + } + + @Test + public void require_that_handler_gives_error_for_unsupported_methods() throws Exception { + testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.POST, Cmd.PREPARED, 1L)); + testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.DELETE, Cmd.PREPARED, 1L)); + testUnsupportedMethod(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.GET, Cmd.PREPARED, 1L)); + } + + @Test + @Ignore + public void require_that_handler_gives_error_when_provisioner_activated_fails() throws Exception { + hostProvisioner = new FailingMockProvisioner(); + hostProvisioner.activated = false; + activateAndAssertError(1, 0, HttpErrorResponse.errorCodes.BAD_REQUEST, "Cannot activate application"); + assertFalse(hostProvisioner.activated); + } + + @Override + protected RemoteSession activateAndAssertOK(long sessionId, long previousSessionId) throws Exception { + ActivateRequest activateRequest = activateAndAssertOKPut(sessionId, previousSessionId, ""); + return activateRequest.getSession(); + } + + @Override + protected Session activateAndAssertOK(long sessionId, long previousSessionId, String subPath) throws Exception { + ActivateRequest activateRequest = activateAndAssertOKPut(sessionId, previousSessionId, subPath); + return activateRequest.getSession(); + } + + @Override + protected void assertActivationMessageOK(ActivateRequest activateRequest, String message) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + new JsonFormat(true).encode(byteArrayOutputStream, activateRequest.getMetaData().getSlime()); + assertThat(message, containsString("\"tenant\":\"" + tenant + "\",\"message\":\"Session " + activateRequest.getSessionId() + activatedMessage)); + assertThat(message, containsString("/application/v2/tenant/" + tenant + + "/application/" + appName + + "/environment/" + "prod" + + "/region/" + "default" + + "/instance/" + "default")); + assertTrue(hostProvisioner.activated); + assertThat(hostProvisioner.lastHosts.size(), is(1)); + } + + @Override + protected void activateAndAssertError(long sessionId, long previousSessionId, HttpErrorResponse.errorCodes errorCode, String expectedError) throws Exception { + hostProvisioner.activated = false; + activateAndAssertErrorPut(sessionId, previousSessionId, errorCode, expectedError); + assertFalse(hostProvisioner.activated); + } + + @Override + protected void writeApplicationId(SessionZooKeeperClient zkc, String applicationName) { + ApplicationId id = ApplicationId.from(tenant, + ApplicationName.from(applicationName), InstanceName.defaultName()); + zkc.writeApplicationId(id); + } + + @Override + protected String getActivateLogPre() { + return "tenant:testtenant, app:default:default "; + } + + @Override + protected SessionHandler createHandler() throws Exception { + final MockSessionFactory sessionFactory = new MockSessionFactory(); + TestTenantBuilder testTenantBuilder = new TestTenantBuilder(); + testTenantBuilder.createTenant(tenant) + .withSessionFactory(sessionFactory) + .withLocalSessionRepo(localRepo) + .withRemoteSessionRepo(remoteSessionRepo) + .withApplicationRepo(applicationRepo) + .build(); + return new SessionActiveHandler(new Executor() { + @SuppressWarnings("NullableProblems") + @Override + public void execute(Runnable command) { + command.run(); + } + }, AccessLog.voidAccessLog(), testTenantBuilder.createTenants(), HostProvisionerProvider.withProvisioner(hostProvisioner), Zone.defaultZone()); + } + + public static class MockProvisioner implements Provisioner { + + boolean activated = false; + boolean removed = false; + boolean restarted = false; + ApplicationId lastApplicationId; + Collection<HostSpec> lastHosts; + + @Override + public List<HostSpec> prepare(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) { + throw new UnsupportedOperationException(); + } + + @Override + public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) { + transaction.commit(); + activated = true; + lastApplicationId = application; + lastHosts = hosts; + } + + @Override + public void removed(ApplicationId application) { + removed = true; + lastApplicationId = application; + } + + @Override + public void restart(ApplicationId application, HostFilter filter) { + restarted = true; + lastApplicationId = application; + } + + } + + public static class FailingMockProvisioner extends MockProvisioner { + + @Override + public void activate(NestedTransaction transaction, ApplicationId application, Collection<HostSpec> hosts) { + throw new IllegalArgumentException("Cannot activate application"); + } + + @Override + public void removed(ApplicationId application) { + throw new IllegalArgumentException("Cannot remove application"); + } + + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java new file mode 100644 index 00000000000..3ca2f2304cc --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandlerTest.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.vespa.config.server.http.SessionContentHandlerTestBase; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import org.junit.Before; + +import java.io.InputStream; +import java.util.concurrent.Executor; + +/** + * @author lulf + * @since 5.1 + */ +public class SessionContentHandlerTest extends SessionContentHandlerTestBase { + private static final TenantName tenant = TenantName.from("contenttest"); + private SessionContentHandler handler = null; + + @Before + public void setupHandler() throws Exception { + handler = createHandler(); + pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; + baseUrl = "http://foo:1337/application/v2/tenant/" + tenant + "/session/1/content/"; + } + + protected HttpResponse doRequest(HttpRequest.Method method, String path) { + return doRequest(method, path, 1l); + } + + protected HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId) { + return handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, method, Cmd.CONTENT, sessionId, path)); + } + + protected HttpResponse doRequest(HttpRequest.Method method, String path, InputStream data) { + return doRequest(method, path, 1l, data); + } + + protected HttpResponse doRequest(HttpRequest.Method method, String path, long sessionId, InputStream data) { + return handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, method, Cmd.CONTENT, sessionId, path, data)); + } + + private SessionContentHandler createHandler() throws Exception { + TestTenantBuilder testTenantBuilder = new TestTenantBuilder(); + testTenantBuilder.createTenant(tenant).getLocalSessionRepo().addSession(new MockSession(1l, FilesApplicationPackage.fromFile(createTestApp()))); + return new SessionContentHandler(new Executor() { + @SuppressWarnings("NullableProblems") + @Override + public void execute(Runnable command) { + command.run(); + } + }, AccessLog.voidAccessLog(), testTenantBuilder.createTenants()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java new file mode 100644 index 00000000000..d31cdc1d1e1 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandlerTest.java @@ -0,0 +1,135 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.server.*; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.config.server.session.*; +import org.junit.Before; +import org.junit.Test; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; + +import static org.junit.Assert.*; + +import static com.yahoo.jdisc.http.HttpRequest.Method.*; + +/** + * @author musum + * @since 5.1 + */ +public class SessionCreateHandlerTest extends SessionCreateHandlerTestBase { + + private static final TenantName tenant = TenantName.from("test"); + + @Before + public void setupRepo() throws Exception { + applicationRepo = new MemoryApplicationRepo(); + localSessionRepo = new LocalSessionRepo(applicationRepo); + pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; + createdMessage = " for tenant '" + tenant + "' created.\""; + tenantMessage = ",\"tenant\":\"test\""; + } + + @Test + public void require_that_application_urls_can_be_given_as_from_parameter() throws Exception { + localSessionRepo.addSession(new SessionHandlerTest.MockSession(2l, FilesApplicationPackage.fromFile(testApp))); + ApplicationId fooId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("foo") + .instanceName("quux") + .build(); + applicationRepo.createPutApplicationTransaction(fooId, 2).commit(); + assertFromParameter("3", "http://myhost:40555/application/v2/tenant/" + tenant + "/application/foo/environment/test/region/baz/instance/quux"); + localSessionRepo.addSession(new SessionHandlerTest.MockSession(5l, FilesApplicationPackage.fromFile(testApp))); + ApplicationId bioId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("foobio") + .instanceName("quux") + .build(); + applicationRepo.createPutApplicationTransaction(bioId, 5).commit(); + assertFromParameter("6", "http://myhost:40555/application/v2/tenant/" + tenant + "/application/foobio/environment/staging/region/baz/instance/quux"); + } + + @Test + public void require_that_from_parameter_must_be_valid() throws IOException { + assertIllegalFromParameter("active"); + assertIllegalFromParameter(""); + assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/lol"); + assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod"); + assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz"); + assertIllegalFromParameter("http://host:4013/application/v2/tenant/" + tenant + "/application/foo/environment/prod/region/baz/instance"); + } + + @Override + public SessionCreateHandler createHandler() { + try { + return createHandler(new MockSessionFactory()); + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + return null; + } + + @Override + public SessionCreateHandler createHandler(SessionFactory sessionFactory) { + try { + TestTenantBuilder testBuilder = new TestTenantBuilder(); + testBuilder.createTenant(tenant).withSessionFactory(sessionFactory) + .withLocalSessionRepo(localSessionRepo) + .withApplicationRepo(applicationRepo); + return createHandler(testBuilder.createTenants()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + SessionCreateHandler createHandler(Tenants tenants) { + return new SessionCreateHandler(new Executor() { + @SuppressWarnings("NullableProblems") + @Override + public void execute(Runnable command) { + command.run(); + } + }, AccessLog.voidAccessLog(), tenants, new ConfigserverConfig(new ConfigserverConfig.Builder())); + } + + @Override + public HttpRequest post() throws FileNotFoundException { + return post(null, postHeaders, new HashMap<String, String>()); + } + + @Override + public HttpRequest post(File file) throws FileNotFoundException { + return post(file, postHeaders, new HashMap<String, String>()); + } + + @Override + public HttpRequest post(File file, Map<String, String> headers, Map<String, String> parameters) throws FileNotFoundException { + HttpRequest request = HttpRequest.createTestRequest("http://" + hostname + ":" + port + "/application/v2/tenant/" + tenant + "/session", + POST, + file == null ? null : new FileInputStream(file), + parameters); + for (Map.Entry<String, String> entry : headers.entrySet()) { + request.getJDiscRequest().headers().put(entry.getKey(), entry.getValue()); + } + return request; + } + + @Override + public HttpRequest post(Map<String, String> parameters) throws FileNotFoundException { + return post(null, new HashMap<String, String>(), parameters); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java new file mode 100644 index 00000000000..0a5d4a1843c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java @@ -0,0 +1,246 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.OutOfCapacityException; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.path.Path; +import com.yahoo.slime.JsonDecoder; +import com.yahoo.slime.Slime; +import com.yahoo.transaction.Transaction; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.HostRegistry; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.config.server.configchange.MockRefeedAction; +import com.yahoo.vespa.config.server.configchange.MockRestartAction; +import com.yahoo.vespa.config.server.http.*; +import com.yahoo.vespa.config.server.session.*; +import com.yahoo.vespa.curator.mock.MockCurator; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author musum + * + * @since 5.1.14 + */ +public class SessionPrepareHandlerTest extends SessionPrepareHandlerTestBase { + private static final TenantName tenant = TenantName.from("test"); + private TestTenantBuilder builder; + + @Before + public void setupRepo() throws Exception { + ApplicationRepo applicationRepo = new MemoryApplicationRepo(); + curator = new MockCurator(); + localRepo = new LocalSessionRepo(applicationRepo); + pathPrefix = "/application/v2/tenant/" + tenant + "/session/"; + preparedMessage = " for tenant '" + tenant + "' prepared.\""; + tenantMessage = ",\"tenant\":\"" + tenant + "\""; + builder = new TestTenantBuilder(); + } + + @Test + public void require_that_tenant_is_in_response() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L)); + assertNotNull(response); + assertThat(response.getStatus(), is(OK)); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + assertResponseContains(response, tenantMessage); + } + + @Test + public void require_that_preparing_with_multiple_tenants_work() throws Exception { + ApplicationRepo applicationRepoDefault = new MemoryApplicationRepo(); + LocalSessionRepo localRepoDefault = new LocalSessionRepo(applicationRepoDefault); + final TenantName tenantName = TenantName.defaultName(); + addTenant(tenantName, localRepoDefault, new RemoteSessionRepo(), new SessionCreateHandlerTestBase.MockSessionFactory()); + addTestTenant(); + final SessionHandler handler = createHandler(builder); + + long sessionId = 1; + // Deploy with default tenant + MockSession session = new MockSession(sessionId, null); + localRepoDefault.addSession(session); + pathPrefix = "/application/v2/tenant/default/session/"; + + HttpResponse response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, sessionId)); + assertNotNull(response); + assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK)); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + + // Same session id, as this is for another tenant + session = new MockSession(sessionId, null); + localRepo.addSession(session); + String applicationName = "myapp"; + pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId + "/prepared?applicationName=" + applicationName; + response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); + assertNotNull(response); + assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK)); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + + sessionId++; + session = new MockSession(sessionId, null); + localRepo.addSession(session); + pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId + "/prepared?applicationName=" + applicationName + "&instance=quux"; + response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); + assertNotNull(response); + assertThat(SessionHandlerTest.getRenderedString(response), response.getStatus(), is(OK)); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + } + + @Test + public void require_that_config_change_actions_are_in_response() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L)); + assertResponseContains(response, "\"configChangeActions\":{\"restart\":[],\"refeed\":[]}"); + } + + @Test + public void require_that_config_change_actions_are_logged_if_existing() throws Exception { + List<ServiceInfo> services = Collections.singletonList(new ServiceInfo("serviceName", "serviceType", null, + ImmutableMap.of("clustername", "foo", "clustertype", "bar"), "configId", "hostName")); + ConfigChangeActions actions = new ConfigChangeActions(Arrays.asList( + new MockRestartAction("change", services), + new MockRefeedAction("change-id", false, "other change", services, "test"))); + MockSession session = new MockSession(1, null, actions); + localRepo.addSession(session); + HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L)); + assertResponseContains(response, "Change(s) between active and new application that require restart:\\nIn cluster 'foo' of type 'bar"); + assertResponseContains(response, "Change(s) between active and new application that may require re-feed:\\nchange-id: Consider removing data and re-feed document type 'test'"); + } + + @Test + public void require_that_config_change_actions_are_not_logged_if_not_existing() throws Exception { + MockSession session = new MockSession(1, null); + localRepo.addSession(session); + HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L)); + assertResponseNotContains(response, "Change(s) between active and new application that require restart"); + assertResponseNotContains(response, "Change(s) between active and new application that require re-feed"); + } + + @Test + public void test_out_of_capacity_response() throws InterruptedException, IOException { + String message = "No nodes available"; + SessionThrowingException session = new SessionThrowingException(new OutOfCapacityException(message)); + localRepo.addSession(session); + HttpResponse response = createHandler(addTestTenant()).handle(SessionHandlerTest.createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, 1L)); + assertEquals(400, response.getStatus()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.render(baos); + Slime data = new Slime(); + new JsonDecoder().decode(data, baos.toByteArray()); + assertThat(data.get().field("error-code").asString(), is(HttpErrorResponse.errorCodes.OUT_OF_CAPACITY.name())); + assertThat(data.get().field("message").asString(), is(message)); + } + + @Override + public SessionHandler createHandler() throws Exception { + return createHandler(addTestTenant()); + } + + @Override + public SessionHandler createHandler(RemoteSessionRepo remoteSessionRepo) throws Exception { + return createHandler(addTenant(tenant, localRepo, remoteSessionRepo, + new SessionCreateHandlerTestBase.MockSessionFactory())); + } + + private TestTenantBuilder addTestTenant() { + return addTenant(tenant, localRepo, new RemoteSessionRepo(), + new SessionCreateHandlerTestBase.MockSessionFactory()); + } + + static SessionHandler createHandler(TestTenantBuilder builder) { + return new SessionPrepareHandler(new Executor() { + @SuppressWarnings("NullableProblems") + @Override + public void execute(Runnable command) { + command.run(); + }}, AccessLog.voidAccessLog(), builder.createTenants(), new ConfigserverConfig(new ConfigserverConfig.Builder())); + } + + private TestTenantBuilder addTenant(TenantName tenantName, + LocalSessionRepo localSessionRepo, + RemoteSessionRepo remoteSessionRepo, + SessionFactory sessionFactory) { + builder.createTenant(tenantName).withSessionFactory(sessionFactory) + .withLocalSessionRepo(localSessionRepo) + .withRemoteSessionRepo(remoteSessionRepo) + .withApplicationRepo(new MemoryApplicationRepo()); + return builder; + } + + public static class SessionThrowingException extends LocalSession { + private final RuntimeException exception; + + public SessionThrowingException(RuntimeException exception) { + super(TenantName.defaultName(), 1, null, new SessionContext(null, new MockSessionZKClient(MockApplicationPackage.createEmpty()), null, null, new HostRegistry<>(), null)); + this.exception = exception; + } + + @Override + public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> application, Path tenantPath) { + throw exception; + } + + @Override + public Session.Status getStatus() { + return null; + } + + @Override + public Transaction createDeactivateTransaction() { + return null; + } + + @Override + public Transaction createActivateTransaction() { + return null; + } + + @Override + public ApplicationFile getApplicationFile(Path relativePath, Mode mode) { + return null; + } + + @Override + public ApplicationId getApplicationId() { + return null; + } + + @Override + public long getCreateTime() { + return 0; + } + + @Override + public void delete() { } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java new file mode 100644 index 00000000000..106b675c2c7 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantHandlerTest.java @@ -0,0 +1,115 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +import java.io.IOException; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.config.server.*; +import org.junit.Before; +import org.junit.Test; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.vespa.config.server.http.BadRequestException; +import com.yahoo.vespa.config.server.http.NotFoundException; + +public class TenantHandlerTest extends TenantTest { + + private TenantHandler handler; + private final TenantName a = TenantName.from("a"); + + @Before + public void setup() throws Exception { + handler = new TenantHandler(testExecutor(), null, tenants); + } + + @Test + public void testTenantCreate() throws Exception { + assertFalse(tenants.tenantsCopy().containsKey(a)); + TenantCreateResponse response = (TenantCreateResponse) putSync(a, + HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT)); + assertResponseEquals(response, "{\"message\":\"Tenant a created.\"}"); + } + + @Test + public void testTenantCreateWithAllPossibleCharactersInName() throws Exception { + TenantName tenantName = TenantName.from("aB-9999_foo"); + assertFalse(tenants.tenantsCopy().containsKey(tenantName)); + TenantCreateResponse response = (TenantCreateResponse) putSync(a, + HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + tenantName, Method.PUT)); + assertResponseEquals(response, "{\"message\":\"Tenant " + tenantName + " created.\"}"); + } + + private HttpResponse putSync(TenantName name, HttpRequest testRequest) throws InterruptedException { + HttpResponse response = handler.handlePUT(testRequest); + return response; + } + + @Test(expected=NotFoundException.class) + public void testGetNonExisting() throws Exception { + handler.handleGET(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/x", Method.GET)); + } + + @Test + public void testGetExisting() throws Exception { + tenants.createTenant(a); + TenantGetResponse response = (TenantGetResponse) handler.handleGET(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.GET)); + assertResponseEquals(response, "{\"message\":\"Tenant 'a' exists.\"}"); + } + + @Test(expected=BadRequestException.class) + public void testCreateExisting() throws Exception { + assertFalse(tenants.tenantsCopy().containsKey(a)); + TenantCreateResponse response = (TenantCreateResponse) putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT)); + assertResponseEquals(response, "{\"message\":\"Tenant a created.\"}"); + Tenant ta = tenants.tenantsCopy().get(a); + assertEquals(ta.getName(), a); + handler.handlePUT(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT)); + } + + @Test + public void testDelete() throws IOException, InterruptedException { + putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.PUT)); + assertEquals(tenants.tenantsCopy().get(a).getName(), a); + TenantDeleteResponse delResp = (TenantDeleteResponse) handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a", Method.DELETE)); + assertResponseEquals(delResp, "{\"message\":\"Tenant a deleted.\"}"); + assertFalse(tenants.tenantsCopy().containsKey(a)); + } + + @Test + public void testDeleteTenantWithActiveApplications() throws Exception { + putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + a, Method.PUT)); + assertEquals(tenants.tenantsCopy().get(a).getName(), a); + + final Tenant tenant = tenants.tenantsCopy().get(a); + final int sessionId = 1; + ApplicationId app = ApplicationId.from(a, + ApplicationName.from("foo"), InstanceName.defaultName()); + ApplicationHandlerTest.addApplication(tenant, app, sessionId); + + try { + handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/" + a, Method.DELETE)); + fail(); + } catch (BadRequestException e) { + assertThat(e.getMessage(), is("Cannot delete tenant 'a', as it has active applications: [tenant 'a', application 'foo', instance 'default']")); + } + } + + @Test(expected=NotFoundException.class) + public void testDeleteNonExisting() { + handler.handleDELETE(HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/x", Method.DELETE)); + } + + @Test(expected=BadRequestException.class) + public void testIllegalNameSlashes() throws InterruptedException { + putSync(a, HttpRequest.createTestRequest("http://deploy.example.yahoo.com:80/application/v2/tenant/a/b", Method.PUT)); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java new file mode 100644 index 00000000000..930787361af --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TenantTest.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.Executor; + +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.http.SessionResponse; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import org.junit.After; +import org.junit.Before; + +/** + * Supertype for tests in the multi tenant application API + * + * @author vegardh + * + */ +public class TenantTest extends TestWithCurator { + + protected Tenants tenants; + + @Before + public void setupTenants() throws Exception { + tenants = createTenants(); + } + + @After + public void closeTenants() throws IOException { + tenants.close(); + } + + protected Tenants createTenants() throws Exception { + return new Tenants(new TestComponentRegistry(curator), Metrics.createTestMetrics()); + } + + protected Executor testExecutor() { + return new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + } + + protected void assertResponseEquals(SessionResponse response, String payload) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.render(baos); + assertEquals(baos.toString("UTF-8"), payload); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java new file mode 100644 index 00000000000..a128fa6c891 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/TestTenantBuilder.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.http.SessionCreateHandlerTestBase; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; +import com.yahoo.vespa.curator.mock.MockCurator; + +import java.util.*; + +/** + * Test utility for creating tenants used for testing and setup wiring of tenant stuff. + * + * @author lulf + * @since 5.1 + */ +public class TestTenantBuilder { + + private GlobalComponentRegistry componentRegistry; + private Map<TenantName, TenantBuilder> tenantMap = new HashMap<>(); + + public TestTenantBuilder() throws Exception { + componentRegistry = new TestComponentRegistry(new MockCurator()); + } + + public TenantBuilder createTenant(TenantName tenantName) { + MemoryApplicationRepo applicationRepo = new MemoryApplicationRepo(); + TenantBuilder builder = TenantBuilder.create(componentRegistry, tenantName, Path.createRoot().append(tenantName.value())) + .withSessionFactory(new SessionCreateHandlerTestBase.MockSessionFactory()) + .withLocalSessionRepo(new LocalSessionRepo(applicationRepo)) + .withRemoteSessionRepo(new RemoteSessionRepo()) + .withApplicationRepo(applicationRepo); + tenantMap.put(tenantName, builder); + return builder; + } + + public Map<TenantName, TenantBuilder> tenants() { + return Collections.unmodifiableMap(tenantMap); + } + + public Tenants createTenants() { + Collection<Tenant> tenantList = Collections2.transform(tenantMap.values(), new Function<TenantBuilder, Tenant>() { + @Override + public Tenant apply(TenantBuilder builder) { + try { + return builder.build(); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to build tenant", e); + } + } + }); + return new Tenants(componentRegistry, Metrics.createTestMetrics(), tenantList); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java new file mode 100644 index 00000000000..d35f7abac5f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/LbServicesProducerTest.java @@ -0,0 +1,206 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.deploy.DeployProperties; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.model.VespaModel; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.*; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.26 + */ +public class LbServicesProducerTest { + private static final String rotation1 = "rotation-1"; + private static final String rotation2 = "rotation-2"; + private static final String rotationString = rotation1 + "," + rotation2; + private static final Set<Rotation> rotations = Collections.singleton(new Rotation(rotationString)); + + @Test + public void testDeterministicGetConfig() throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder().rotations(rotations)); + LbServicesConfig last = null; + for (int i = 0; i < 100; i++) { + testModel = randomizeTenant(testModel, i); + LbServicesConfig config = getLbServicesConfig(Zone.defaultZone(), testModel); + if (last != null) { + assertConfig(last, config); + } + last = config; + } + } + + @Test + public void testConfigAliases() throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder()); + LbServicesConfig conf = getLbServicesConfig(Zone.defaultZone(), testModel); + final LbServicesConfig.Tenants.Applications.Hosts.Services services = conf.tenants("foo").applications("foo:prod:default:default").hosts("foo.foo.yahoo.com").services("qrserver"); + assertThat(services.servicealiases().size(), is(1)); + assertThat(services.endpointaliases().size(), is(2)); + + assertThat(services.servicealiases(0), is("service1")); + assertThat(services.endpointaliases(0), is("foo1.bar1.com")); + assertThat(services.endpointaliases(1), is("foo2.bar2.com")); + } + + @Test + public void testConfigActiveRotation() throws IOException, SAXException { + { + RegionName regionName = RegionName.from("us-east-1"); + LbServicesConfig conf = createModelAndGetLbServicesConfig(regionName); + assertTrue(conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").activeRotation()); + } + + { + RegionName regionName = RegionName.from("us-east-2"); + LbServicesConfig conf = createModelAndGetLbServicesConfig(regionName); + assertFalse(conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").activeRotation()); + } + } + + private LbServicesConfig createModelAndGetLbServicesConfig(RegionName regionName) throws IOException, SAXException { + final Zone zone = new Zone(Environment.prod, regionName); + Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder(). + properties(new DeployProperties.Builder().zone(zone).build())); + return getLbServicesConfig(new Zone(Environment.prod, regionName), testModel); + } + + private LbServicesConfig getLbServicesConfig(Zone zone, Map<TenantName, Map<ApplicationId, Application>> testModel) { + LbServicesProducer producer = new LbServicesProducer(testModel, zone); + LbServicesConfig.Builder builder = new LbServicesConfig.Builder(); + producer.getConfig(builder); + return new LbServicesConfig(builder); + } + + @Test + public void testConfigAliasesWithRotations() throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder().rotations(rotations)); + RegionName regionName = RegionName.from("us-east-1"); + LbServicesConfig conf = getLbServicesConfig(new Zone(Environment.prod, regionName), testModel); + final LbServicesConfig.Tenants.Applications.Hosts.Services services = conf.tenants("foo").applications("foo:prod:" + regionName.value() + ":default").hosts("foo.foo.yahoo.com").services("qrserver"); + assertThat(services.servicealiases().size(), is(1)); + assertThat(services.endpointaliases().size(), is(4)); + + assertThat(services.servicealiases(0), is("service1")); + assertThat(services.endpointaliases(0), is("foo1.bar1.com")); + assertThat(services.endpointaliases(1), is("foo2.bar2.com")); + assertThat(services.endpointaliases(2), is(rotation1)); + assertThat(services.endpointaliases(3), is(rotation2)); + } + + private Map<TenantName, Map<ApplicationId, Application>> randomizeTenant(Map<TenantName, Map<ApplicationId, Application>> testModel, int seed) { + Map<TenantName, Map<ApplicationId, Application>> randomizedTenants = new LinkedHashMap<>(); + List<TenantName> keys = new ArrayList<>(testModel.keySet()); + Collections.shuffle(keys, new Random(seed)); + for (TenantName key : keys) { + randomizedTenants.put(key, randomizeApplications(testModel.get(key), randomizedTenants.size())); + } + return randomizedTenants; + } + + private Map<ApplicationId, Application> randomizeApplications(Map<ApplicationId, Application> applicationIdApplicationMap, int seed) { + Map<ApplicationId, Application> randomizedApplications = new LinkedHashMap<>(); + List<ApplicationId> keys = new ArrayList<>(applicationIdApplicationMap.keySet()); + Collections.shuffle(keys, new Random(seed)); + for (ApplicationId key : keys) { + randomizedApplications.put(key, applicationIdApplicationMap.get(key)); + } + return randomizedApplications; + } + + private Map<TenantName, Map<ApplicationId, Application>> createTestModel(DeployState.Builder deployStateBuilder) throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> tMap = new LinkedHashMap<>(); + TenantName foo = TenantName.from("foo"); + TenantName bar = TenantName.from("bar"); + TenantName baz = TenantName.from("baz"); + tMap.put(foo, createTestApplications(foo, deployStateBuilder)); + tMap.put(bar, createTestApplications(bar, deployStateBuilder)); + tMap.put(baz, createTestApplications(baz, deployStateBuilder)); + return tMap; + } + + private Map<ApplicationId, Application> createTestApplications(TenantName tenant, DeployState.Builder deploystateBuilder) throws IOException, SAXException { + Map<ApplicationId, Application> aMap = new LinkedHashMap<>(); + ApplicationId fooApp = new ApplicationId.Builder().tenant(tenant).applicationName("foo").build(); + ApplicationId barApp = new ApplicationId.Builder().tenant(tenant).applicationName("bar").build(); + ApplicationId bazApp = new ApplicationId.Builder().tenant(tenant).applicationName("baz").build(); + aMap.put(fooApp, createApplication(fooApp, deploystateBuilder)); + aMap.put(barApp, createApplication(barApp, deploystateBuilder)); + aMap.put(bazApp, createApplication(bazApp, deploystateBuilder)); + return aMap; + } + + private Application createApplication(ApplicationId appId, DeployState.Builder deploystateBuilder) throws IOException, SAXException { + return new Application(createVespaModel(createApplicationPackage( + appId.tenant() + "." + appId.application() + ".yahoo.com", appId.tenant().value() + "." + appId.application().value() + "2.yahoo.com"), + deploystateBuilder), + new ServerCache(), + 3l, + Version.fromIntValues(1, 2, 3), + MetricUpdater.createTestUpdater(), + appId); + } + + private ApplicationPackage createApplicationPackage(String host1, String host2) { + String hosts = "<hosts><host name='" + host1 + "'><alias>node1</alias></host><host name='" + host2 + "'><alias>node2</alias></host></hosts>"; + String services = "<services><admin version='2.0'><adminserver hostalias='node1' /><logserver hostalias='node1' /><slobroks><slobrok hostalias='node1' /><slobrok hostalias='node2' /></slobroks></admin>" + + "<jdisc id='mydisc' version='1.0'>" + + " <aliases>" + + " <endpoint-alias>foo2.bar2.com</endpoint-alias>" + + " <service-alias>service1</service-alias>" + + " <endpoint-alias>foo1.bar1.com</endpoint-alias>" + + " </aliases>" + + " <nodes>" + + " <node hostalias='node1' />" + + " </nodes>" + + " <search/>" + + "</jdisc>" + + "</services>"; + String deploymentInfo ="<?xml version='1.0' encoding='UTF-8'?>" + + "<deployment version='1.0'>" + + " <test />" + + " <prod global-service-id='mydisc'>" + + " <region active='true'>us-east-1</region>" + + " <region active='false'>us-east-2</region>" + + " </prod>" + + "</deployment>"; + + return new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).withDeploymentInfo(deploymentInfo).build(); + } + + private Model createVespaModel(ApplicationPackage applicationPackage, DeployState.Builder deployStateBuilder) throws IOException, SAXException { + return new VespaModel(new NullConfigModelRegistry(), deployStateBuilder.applicationPackage(applicationPackage).build()); + } + + private void assertConfig(LbServicesConfig expected, LbServicesConfig actual) { + assertFalse(expected.toString().isEmpty()); + assertFalse(actual.toString().isEmpty()); + assertThat(expected.toString(), is(actual.toString())); + assertThat(ConfigPayload.fromInstance(expected).toString(true), is(ConfigPayload.fromInstance(actual).toString(true))); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java new file mode 100755 index 00000000000..a8263cd361a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/RoutingProducerTest.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import com.yahoo.cloud.config.RoutingConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.model.VespaModel; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author can + */ +public class RoutingProducerTest { + @Test + public void testNodesFromRoutingAppOnly() throws Exception { + Map<TenantName, Map<ApplicationId, Application>> testModel = createTestModel(new DeployState.Builder()); + RoutingProducer producer = new RoutingProducer(testModel); + RoutingConfig.Builder builder = new RoutingConfig.Builder(); + producer.getConfig(builder); + RoutingConfig config = new RoutingConfig(builder); + assertThat(config.hosts().size(), is(2)); + assertThat(config.hosts(0), is("hosted-vespa.routing.yahoo.com")); + assertThat(config.hosts(1), is("hosted-vespa.routing2.yahoo.com")); + } + + private Map<TenantName, Map<ApplicationId, Application>> createTestModel(DeployState.Builder deployStateBuilder) throws IOException, SAXException { + Map<TenantName, Map<ApplicationId, Application>> tMap = new LinkedHashMap<>(); + TenantName foo = TenantName.from("foo"); + TenantName bar = TenantName.from("bar"); + TenantName routing = TenantName.from(ApplicationId.HOSTED_VESPA_TENANT.value()); + tMap.put(foo, createTestApplications(foo, deployStateBuilder)); + tMap.put(bar, createTestApplications(bar, deployStateBuilder)); + tMap.put(routing, createTestApplications(routing, deployStateBuilder)); + return tMap; + } + + private Map<ApplicationId, Application> createTestApplications(TenantName tenant, DeployState.Builder deploystateBuilder) throws IOException, SAXException { + Map<ApplicationId, Application> aMap = new LinkedHashMap<>(); + ApplicationId fooApp = new ApplicationId.Builder().tenant(tenant).applicationName("foo").build(); + ApplicationId barApp = new ApplicationId.Builder().tenant(tenant).applicationName("bar").build(); + ApplicationId routingApp = new ApplicationId.Builder().tenant(tenant).applicationName(ApplicationId.ROUTING_APPLICATION.value()).build(); + aMap.put(fooApp, createApplication(fooApp, deploystateBuilder)); + aMap.put(barApp, createApplication(barApp, deploystateBuilder)); + aMap.put(routingApp, createApplication(routingApp, deploystateBuilder)); + return aMap; + } + + private Application createApplication(ApplicationId appId, DeployState.Builder deploystateBuilder) throws IOException, SAXException { + return new Application(createVespaModel(createApplicationPackage( + appId.tenant() + "." + appId.application() + ".yahoo.com", appId.tenant().value() + "." + appId.application().value() + "2.yahoo.com"), + deploystateBuilder), + new ServerCache(), + 3l, + Version.fromIntValues(1, 2, 3), + MetricUpdater.createTestUpdater(), + appId); + } + + private ApplicationPackage createApplicationPackage(String host1, String host2) { + String hosts = "<hosts><host name='" + host1 + "'><alias>node1</alias></host><host name='" + host2 + "'><alias>node2</alias></host></hosts>"; + String services = "<services><admin version='2.0'><adminserver hostalias='node1' /><logserver hostalias='node1' /><slobroks><slobrok hostalias='node1' /><slobrok hostalias='node2' /></slobroks></admin>" + + "<jdisc id='mydisc' version='1.0'>" + + " <aliases>" + + " <endpoint-alias>foo2.bar2.com</endpoint-alias>" + + " <service-alias>service1</service-alias>" + + " <endpoint-alias>foo1.bar1.com</endpoint-alias>" + + " </aliases>" + + " <nodes>" + + " <node hostalias='node1' />" + + " </nodes>" + + " <search/>" + + "</jdisc>" + + "</services>"; + String deploymentInfo ="<?xml version='1.0' encoding='UTF-8'?>" + + "<deployment version='1.0'>" + + " <test />" + + " <prod global-service-id='mydisc'>" + + " <region active='true'>us-east</region>" + + " </prod>" + + "</deployment>"; + + return new MockApplicationPackage.Builder() + .withHosts(hosts) + .withServices(services) + .withDeploymentInfo(deploymentInfo) + .build(); + } + + private Model createVespaModel(ApplicationPackage applicationPackage, DeployState.Builder deployStateBuilder) throws IOException, SAXException { + return new VespaModel(new NullConfigModelRegistry(), deployStateBuilder.applicationPackage(applicationPackage).build()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java b/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java new file mode 100644 index 00000000000..32d1b610194 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/model/TestModelFactory.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelCreateResult; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.model.VespaModelFactory; + +/** + * @author lulf + */ +public class TestModelFactory extends VespaModelFactory { + private final Version vespaVersion; + private ModelContext modelContext; + + public TestModelFactory(Version vespaVersion) { + super(new NullConfigModelRegistry()); + this.vespaVersion = vespaVersion; + } + + // Needed for testing (to get hold of ModelContext) + @Override + public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) { + this.modelContext = modelContext; + return super.createAndValidateModel(modelContext, ignoreValidationErrors); + } + + @Override + public Version getVersion() { + return vespaVersion; + } + + public ModelContext getModelContext() { + return modelContext; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg new file mode 100644 index 00000000000..d3970ee48eb --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustermusic-c0-r0-indexer4.cfg @@ -0,0 +1,44 @@ +include: search/cluster.music +include: search/cluster.music +c 2 +storage[2] +storage[0].feeder[1] +storage[0].feeder[0] "test" +storage[1].feeder[2] +storage[1].feeder[0] "me" +storage[1].feeder[1] now +storage[1].id :parent: +storage[1].id2 pjatt +testref :parent: +testref2 some/babbel +config[1] +config[0].role "rtx" +#config[0].usewrapper false +config[0].id search/cluster.music/rtx/0 +f[1] +f[0].a "A" +f[0].b "B" +f[0].c "C" +f[0].h "H" +f[0].f "F" +f[0].notindef "notindef" +routingtable[1] +routingtable[0].hop[3] +routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.music.indexing" +routingtable[0].hop[0].selector "docproc/cluster.music.indexing/*/chain.music.indexing" +routingtable[0].hop[1].name "search/cluster.music" +routingtable[0].hop[1].selector "search/cluster.music/[SearchColumn]/[SearchRow]/feed-destination" +routingtable[0].hop[1].recipient[1] +routingtable[0].hop[1].recipient[0] "search/cluster.music/c0/r0/feed-destination" +routingtable[0].hop[2].selector "[DocumentRouteSelector]" +routingtable[0].hop[2].name "indexing" +routingtable[0].hop[2].notindef "not in def" +routingtable[0].hop[2].recipient[1] +routingtable[0].hop[2].recipient[0] "search/cluster.music" +notindef "dfsd" +nopenotindef[0] "boo" +nadaindef[0].naw 98 +mode NOTINDEF +rangecheck1 100 +rangecheck2 10000 +rangecheck3 20 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg new file mode 100644 index 00000000000..727a5052ed6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.search-clustersports-c0-r0-indexer4.cfg @@ -0,0 +1,2 @@ +include: search/cluster.sports +c 67 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg new file mode 100644 index 00000000000..f4996027f60 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/a.vespamodel.cfg @@ -0,0 +1 @@ +model vespa diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg new file mode 100644 index 00000000000..d75d76810f9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/c.search-clustersports-c0-r0-indexer4.cfg @@ -0,0 +1,2 @@ +foo "bar" +gaz -78 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg new file mode 100644 index 00000000000..7ccdb73eb9a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/compositeinclude.search-qrservers-0.cfg @@ -0,0 +1,2 @@ +include: search/cluster.logical/* +include: search/cluster.video/* diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg new file mode 100644 index 00000000000..5b07d3a2890 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/recursiveinclude.search-clustermusic-c0-r0.cfg @@ -0,0 +1 @@ +include: search/cluster.music diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg new file mode 100644 index 00000000000..038d655e83c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/modelconfigs/spooler.clients-spooler-spooler.cfg @@ -0,0 +1,13 @@ +directory /home/vespa/var/spool/vespa/ +keepsuccess false +parsers[4] +parsers[0].classname com.yahoo.vespaspooler.XMLFileParser +parsers[0].parameters[0] +parsers[1].classname com.yahoo.mail.vespa.spooler.MailFileParser +parsers[1].parameters[0] +parsers[2].classname com.yahoo.mail.vespa.spooler.UserDeleteParser +parsers[2].parameters[0] +parsers[3].classname com.yahoo.mail.vespa.spooler.VespaGrimParser +parsers[3].parameters[1] +parsers[3].parameters[0].key chunksize +parsers[3].parameters[0].value 5 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java new file mode 100644 index 00000000000..3831f94a77d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/provision/StaticProvisionerTest.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.provision; + +import com.yahoo.cloud.config.ModelConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.model.deploy.DeployProperties; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.model.provision.InMemoryProvisioner; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.model.VespaModel; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +/** + * @author lulf + */ +public class StaticProvisionerTest { + @Test + public void sameHostsAreProvisioned() throws IOException, SAXException { + ApplicationPackage app = FilesApplicationPackage.fromFile(new File("src/test/apps/hosted")); + InMemoryProvisioner inMemoryHostProvisioner = new InMemoryProvisioner(false, "host1.yahoo.com", "host2.yahoo.com", "host3.yahoo.com", "host4.yahoo.com"); + VespaModel firstModel = createModel(app, inMemoryHostProvisioner); + + StaticProvisioner staticProvisioner = new StaticProvisioner(firstModel.getProvisionInfo().get()); + VespaModel secondModel = createModel(app, staticProvisioner); + + assertModelConfig(firstModel, secondModel); + } + + private void assertModelConfig(VespaModel firstModel, VespaModel secondModel) { + String firstConfig = getModelConfig(firstModel); + String secondConfig = getModelConfig(secondModel); + assertEquals(firstConfig, secondConfig); + } + + private String getModelConfig(VespaModel model) { + return ConfigPayload.fromInstance(model.getConfig(ModelConfig.class, "")).toString(); + } + + private VespaModel createModel(ApplicationPackage app, HostProvisioner provisioner) throws IOException, SAXException { + DeployState deployState = new DeployState.Builder() + .applicationPackage(app) + .modelHostProvisioner(provisioner) + .properties(new DeployProperties.Builder() + .multitenant(true) + .hostedVespa(true) + .build()) + .build(); + return new VespaModel(new NullConfigModelRegistry(), deployState); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java new file mode 100644 index 00000000000..333cac4fd48 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/restapi/impl/StatusResourceTest.java @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.restapi.impl; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.restapi.resources.StatusInformation; +import com.yahoo.vespa.defaults.Defaults; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class StatusResourceTest { + @Test + public void require_that_status_handler_responds_to_ping() throws IOException { + StatusResource handler = new StatusResource(null, null, null, null, null, null, null, new TestComponentRegistry()); + assertNotNull(handler.getStatus().configserverConfig); + } + + @Test + public void require_that_generated_config_is_converted() { + ConfigserverConfig orig = new ConfigserverConfig(new ConfigserverConfig.Builder()); + StatusInformation.ConfigserverConfig conv = new StatusInformation.ConfigserverConfig(orig); + assertThat(conv.applicationDirectory, is(Defaults.getDefaults().underVespaHome(orig.applicationDirectory()))); + assertThat(conv.configModelPluginDir.size(), is(orig.configModelPluginDir().size())); + assertThat(conv.zookeeeperserver.size(), is(orig.zookeeperserver().size())); + assertThat(conv.zookeeperBarrierTimeout, is(orig.zookeeper().barrierTimeout())); + assertThat(conv.configServerDBDir, is(Defaults.getDefaults().underVespaHome(orig.configServerDBDir()))); + assertThat(conv.masterGeneration, is(orig.masterGeneration())); + assertThat(conv.maxgetconfigclients, is(orig.maxgetconfigclients())); + assertThat(conv.multitenant, is(orig.multitenant())); + assertThat(conv.numDelayedResponseThreads, is(orig.numDelayedResponseThreads())); + assertThat(conv.numthreads, is(orig.numthreads())); + assertThat(conv.payloadCompressionType, is(orig.payloadCompressionType())); + assertThat(conv.rpcport, is(orig.rpcport())); + assertThat(conv.sessionLifetime, is(orig.sessionLifetime())); + assertThat(conv.zookeepercfg, is(Defaults.getDefaults().underVespaHome(orig.zookeepercfg()))); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg new file mode 100644 index 00000000000..f3acd4cf8b9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustermusic.cfg @@ -0,0 +1,5 @@ +asyncfetchocc 9 +d 3 +kanon -78.56 + +partialsd "sd" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg new file mode 100644 index 00000000000..5d8a01a18ea --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/a.search-clustersports.cfg @@ -0,0 +1,2 @@ +d 89 +search[1].feeder[1] "sportsfeeder1" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg new file mode 100644 index 00000000000..f6c35df398d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/b.search-clustersports.cfg @@ -0,0 +1 @@ +gaff -89 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg new file mode 100644 index 00000000000..c3d9b1e45a1 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clusterlogical.cfg @@ -0,0 +1,8 @@ +classes[1] +classes[logical].id 1906788747 +classes[logical].name logical +classes[logical].fields[2] +classes[logical].fields[0].name sddocnameNAM +classes[logical].fields[0].type longstring +classes[logical].fields[1].name title +classes[logical].fields[1].type longstringSTRIN diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg new file mode 100644 index 00000000000..12a21671b4a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-clustervideo.cfg @@ -0,0 +1,8 @@ +classes[1] +classes[music].id 1906788746 +classes[music].name music +classes[music].fields[2] +classes[music].fields[0].name sddocnameNAME +classes[music].fields[0].type longstring +classes[music].fields[1].name title +classes[music].fields[1].type longstringSTRING diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg new file mode 100644 index 00000000000..4001c59adbc --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clusterlogical.cfg @@ -0,0 +1,14 @@ +classes[0] +classes[smallsum614540714].id 614540714 +classes[smallsum614540714].name smallsum +classes[smallsum614540714].fields[5] +classes[smallsum614540714].fields[0].name s_13 +classes[smallsum614540714].fields[0].type longstring +classes[smallsum614540714].fields[1].name ranklog +classes[smallsum614540714].fields[1].type longstring +classes[smallsum614540714].fields[2].name rankfeatures +classes[smallsum614540714].fields[2].type longstring +classes[smallsum614540714].fields[3].name summaryfeatures +classes[smallsum614540714].fields[3].type longstring +classes[smallsum614540714].fields[4].name sddocname +classes[smallsum614540714].fields[4].type longstring diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg new file mode 100644 index 00000000000..33d07b99ab6 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/compositeinclude.search-part-clustervideo.cfg @@ -0,0 +1,14 @@ +classes[0] +classes[smallsum507688128].id 507688128 +classes[smallsum507688128].name smallsum +classes[smallsum507688128].fields[5] +classes[smallsum507688128].fields[0].name title +classes[smallsum507688128].fields[0].type longstring +classes[smallsum507688128].fields[1].name ranklog +classes[smallsum507688128].fields[1].type longstring +classes[smallsum507688128].fields[2].name rankfeatures +classes[smallsum507688128].fields[2].type longstring +classes[smallsum507688128].fields[3].name summaryfeatures +classes[smallsum507688128].fields[3].type longstring +classes[smallsum507688128].fields[4].name sddocname +classes[smallsum507688128].fields[4].type longstring diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg new file mode 100644 index 00000000000..de9fbdd39f4 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf1.4.cfg @@ -0,0 +1,7 @@ +rec 56 +national 77 +ilscript[1] +ilscript[music].name music +ilscript[music].doctype music +ilscript[music].content[1] +ilscript[music].content[0] "input year | summary s_3 | tokenize \"stemming,normalizing\" { index f_3 | index f_4; };" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg new file mode 100644 index 00000000000..e95f976a43a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic-conf2.4.cfg @@ -0,0 +1,7 @@ +ursive -50 +teatern 78 +ilscript[1] +ilscript[father].name father +ilscript[father].doctype father +ilscript[father].content[6] +ilscript[father].content[0] "input year | summary s_3 | tokenize \"stemming,normalizing\" { index f_3 | index f_5; };" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg new file mode 100644 index 00000000000..cea943d5bc9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/sdconfigs/recursiveinclude.search-clustermusic.cfg @@ -0,0 +1,2 @@ +include: search/cluster.music/conf1.sd.derived +include: search/cluster.music/conf2.sd.derived diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java new file mode 100644 index 00000000000..ea4455ab99f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/DummyTransaction.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.transaction.Transaction; + +import java.util.ArrayList; +import java.util.List; + +/** + * Dummy transaction implementation that only does stuff in memory and does not adhere to contract. + * @author lulf + */ +public class DummyTransaction implements Transaction { + + private final List<Operation> operations = new ArrayList<>(); + + public interface RunnableOperation extends Operation, Runnable { + } + + public DummyTransaction() { } + + @Override + public Transaction add(Operation operation) { + this.operations.add(operation); + return this; + } + + @Override + public Transaction add(List<Operation> operations) { + this.operations.addAll(operations); + return this; + } + + @Override + public List<Operation> operations() { return new ArrayList<>(operations); } + + @Override + public void prepare() { } + + @Override + public void commit() { + for (Operation op : operations) { + ((RunnableOperation)op).run(); + } + } + + @Override + public void rollbackOrLog() { + throw new IllegalStateException("Unexpected rollback"); + } + + @Override + public void close() { } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java new file mode 100644 index 00000000000..84fce1c09fe --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionRepoTest.java @@ -0,0 +1,127 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.path.Path; +import com.yahoo.test.ManualClock; +import com.yahoo.vespa.config.server.*; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.io.IOUtils; +import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.config.server.zookeeper.SessionCounter; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.time.Duration; +import java.time.Instant; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author lulf + * @since 5.1 + */ +public class LocalSessionRepoTest extends TestWithCurator { + + private File testApp = new File("src/test/apps/app"); + private LocalSessionRepo repo; + private ManualClock clock; + private static final TenantName tenantName = TenantName.defaultName(); + + @Before + public void setupSessions() throws Exception { + setupSessions(tenantName, true); + } + + private void setupSessions(TenantName tenantName, boolean createInitialSessions) throws Exception { + GlobalComponentRegistry globalComponentRegistry = new TestComponentRegistry(curator); + TenantFileSystemDirs tenantFileSystemDirs = TenantFileSystemDirs.createTestDirs(tenantName); + if (createInitialSessions) { + IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "1")); + IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "2")); + IOUtils.copyDirectory(testApp, new File(tenantFileSystemDirs.path(), "3")); + } + clock = new ManualClock(Instant.ofEpochSecond(1)); + LocalSessionLoader loader = new SessionFactoryImpl(globalComponentRegistry, + new SessionCounter(globalComponentRegistry.getCurator(), + Path.fromString("counter"), + Path.fromString("sessions")), + Path.createRoot(), + new MemoryApplicationRepo(), + tenantFileSystemDirs, new HostRegistry<>(), + tenantName); + repo = new LocalSessionRepo(tenantFileSystemDirs, loader, new MemoryApplicationRepo(), clock, 5); + } + + @Test + public void require_that_sessions_can_be_loaded_from_disk() { + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNotNull(repo.getSession(3l)); + assertNull(repo.getSession(4l)); + } + + @Test + public void require_that_old_sessions_are_purged() { + clock.advance(Duration.ofSeconds(1)); + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNotNull(repo.getSession(3l)); + clock.advance(Duration.ofSeconds(1)); + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNotNull(repo.getSession(3l)); + clock.advance(Duration.ofSeconds(1)); + addSession(4l, 6); + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNotNull(repo.getSession(3l)); + assertNotNull(repo.getSession(4l)); + clock.advance(Duration.ofSeconds(1)); + addSession(5l, 10); + assertNull(repo.getSession(1l)); + assertNull(repo.getSession(2l)); + assertNull(repo.getSession(3l)); + } + + @Test + public void require_that_all_sessions_are_deleted() { + repo.deleteAllSessions(); + assertNull(repo.getSession(1l)); + assertNull(repo.getSession(2l)); + assertNull(repo.getSession(3l)); + } + + private void addSession(long sessionId, long createTime) { + repo.addSession(new SessionHandlerTest.MockSession(sessionId, FilesApplicationPackage.fromFile(testApp), createTime)); + } + + @Test + public void require_that_sessions_belong_to_a_tenant() { + // tenant is "default" + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNotNull(repo.getSession(3l)); + assertNull(repo.getSession(4l)); + + // tenant is "newTenant" + try { + setupSessions(TenantName.from("newTenant"), false); + } catch (Exception e) { + fail(); + } + assertNull(repo.getSession(1l)); + + repo.addSession(new SessionHandlerTest.MockSession(1l, FilesApplicationPackage.fromFile(testApp))); + repo.addSession(new SessionHandlerTest.MockSession(2l, FilesApplicationPackage.fromFile(testApp))); + assertNotNull(repo.getSession(1l)); + assertNotNull(repo.getSession(2l)); + assertNull(repo.getSession(3l)); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java new file mode 100644 index 00000000000..4f638d54d46 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/LocalSessionTest.java @@ -0,0 +1,180 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.io.Files; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.provision.*; +import com.yahoo.path.Path; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.deploy.ZooKeeperClient; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.*; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.1 + */ +public class LocalSessionTest { + + private Path tenantPath = Path.createRoot(); + private Curator curator; + private ConfigCurator configCurator; + private TenantFileSystemDirs tenantFileSystemDirs; + private SuperModelGenerationCounter superModelGenerationCounter; + + @Before + public void setupTest() throws Exception { + curator = new MockCurator(); + configCurator = ConfigCurator.create(curator); + superModelGenerationCounter = new SuperModelGenerationCounter(curator); + tenantFileSystemDirs = new TenantFileSystemDirs(Files.createTempDir(), TenantName.from("test_tenant")); + } + + @Test + public void require_that_session_is_initialized() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 2); + assertThat(session.getSessionId(), is(2l)); + session = createSession(TenantName.defaultName(), Long.MAX_VALUE); + assertThat(session.getSessionId(), is(Long.MAX_VALUE)); + assertThat(session.getActiveSessionAtCreate(), is(0l)); + } + + @Test + public void require_that_session_status_is_updated() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 3); + assertThat(session.getStatus(), is(Session.Status.NEW)); + doPrepare(session); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + session.createActivateTransaction().commit(); + assertThat(session.getStatus(), is(Session.Status.ACTIVATE)); + } + + @Test + public void require_that_marking_session_modified_changes_status_to_new() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 3); + doPrepare(session); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + session.getApplicationFile(Path.createRoot(), LocalSession.Mode.READ); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + session.getApplicationFile(Path.createRoot(), LocalSession.Mode.WRITE); + assertThat(session.getStatus(), is(Session.Status.NEW)); + } + + @Test + public void require_that_preparer_is_run() throws Exception { + SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer(); + LocalSession session = createSession(TenantName.defaultName(), 3, preparer); + assertFalse(preparer.isPrepared); + doPrepare(session); + assertTrue(preparer.isPrepared); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + } + + @Test + public void require_that_session_status_can_be_deactivated() throws Exception { + SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer(); + LocalSession session = createSession(TenantName.defaultName(), 3, preparer); + session.createDeactivateTransaction().commit(); + assertThat(session.getStatus(), is(Session.Status.DEACTIVATE)); + } + + private File testApp = new File("src/test/apps/app"); + + @Test + public void require_that_application_file_can_be_fetched() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 3); + ApplicationFile f1 = session.getApplicationFile(Path.fromString("services.xml"), LocalSession.Mode.READ); + ApplicationFile f2 = session.getApplicationFile(Path.fromString("services2.xml"), LocalSession.Mode.READ); + assertTrue(f1.exists()); + assertFalse(f2.exists()); + } + + @Test + public void require_that_session_can_be_deleted() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 3); + assertTrue(configCurator.exists("/3")); + assertTrue(new File(tenantFileSystemDirs.path(), "3").exists()); + long gen = superModelGenerationCounter.get(); + session.delete(); + assertThat(superModelGenerationCounter.get(), is(gen + 1)); + assertFalse(configCurator.exists("/3")); + assertFalse(new File(tenantFileSystemDirs.path(), "3").exists()); + } + + @Test(expected = IllegalStateException.class) + public void require_that_no_provision_info_throws_exception() throws Exception { + createSession(TenantName.defaultName(), 3).getProvisionInfo(); + } + + @Test + public void require_that_provision_info_can_be_read() throws Exception { + ProvisionInfo input = ProvisionInfo.withHosts(Collections.singleton(new HostSpec("myhost", Collections.<String>emptyList()))); + + LocalSession session = createSession(TenantName.defaultName(), 3, new SessionTest.MockSessionPreparer(), Optional.of(input)); + ApplicationId origId = new ApplicationId.Builder() + .tenant("tenant") + .applicationName("foo").instanceName("quux").build(); + doPrepare(session, new PrepareParams().applicationId(origId)); + ProvisionInfo info = session.getProvisionInfo(); + assertNotNull(info); + assertThat(info.getHosts().size(), is(1)); + assertTrue(info.getHosts().contains(new HostSpec("myhost", Collections.emptyList()))); + } + + @Test + public void require_that_application_metadata_is_correct() throws Exception { + LocalSession session = createSession(TenantName.defaultName(), 3); + doPrepare(session, new PrepareParams()); + assertThat(session.getMetaData().toString(), is("n/a, n/a, 0, 0, , 0")); + } + + private LocalSession createSession(TenantName tenant, long sessionId) throws Exception { + SessionTest.MockSessionPreparer preparer = new SessionTest.MockSessionPreparer(); + return createSession(tenant, sessionId, preparer); + } + + private LocalSession createSession(TenantName tenant, long sessionId, SessionTest.MockSessionPreparer preparer) throws Exception { + return createSession(tenant, sessionId, preparer, Optional.<ProvisionInfo>empty()); + } + + private LocalSession createSession(TenantName tenant, long sessionId, SessionTest.MockSessionPreparer preparer, Optional<ProvisionInfo> provisionInfo) throws Exception { + Path appPath = Path.fromString("/" + sessionId); + SessionZooKeeperClient zkc = new MockSessionZKClient(curator, appPath, provisionInfo); + zkc.createWriteStatusTransaction(Session.Status.NEW).commit(); + ZooKeeperClient zkClient = new ZooKeeperClient(configCurator, new BaseDeployLogger(), false, appPath); + if (provisionInfo.isPresent()) { + zkClient.feedProvisionInfos(Collections.singletonMap(Version.fromIntValues(0, 0, 0), provisionInfo.get())); + } + zkClient.feedZKFileRegistries(Collections.singletonMap(Version.fromIntValues(0, 0, 0), new MockFileRegistry())); + File sessionDir = new File(tenantFileSystemDirs.path(), String.valueOf(sessionId)); + sessionDir.createNewFile(); + return new LocalSession(tenant, sessionId, preparer, new SessionContext(FilesApplicationPackage.fromFile(testApp), zkc, sessionDir, new MemoryApplicationRepo(), new HostRegistry<>(), superModelGenerationCounter)); + } + + private void doPrepare(LocalSession session) { + doPrepare(session, new PrepareParams()); + } + + private void doPrepare(LocalSession session, PrepareParams params) { + session.prepare(getLogger(false), params, Optional.empty(), tenantPath); + } + + DeployHandlerLogger getLogger(boolean verbose) { + return new DeployHandlerLogger(new Slime().get(), verbose, + new ApplicationId.Builder().tenant("testtenant").applicationName("testapp").build()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java new file mode 100644 index 00000000000..0af74cc9312 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockFileDistributionFactory.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider; +import com.yahoo.vespa.config.server.filedistribution.MockFileDistributionProvider; +import com.yahoo.vespa.curator.mock.MockCurator; + +import java.io.File; + +/** +* @author lulf +* @since 5.1 +*/ +public class MockFileDistributionFactory extends FileDistributionFactory { + + public final MockFileDistributionProvider mockFileDistributionProvider = new MockFileDistributionProvider(); + + public MockFileDistributionFactory() { + super(new MockCurator(), ""); + } + + @Override + public FileDistributionProvider createProvider(File applicationFile, ApplicationId applicationId) { + return mockFileDistributionProvider; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java new file mode 100644 index 00000000000..829c3f9008b --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/MockSessionZKClient.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.transaction.Transaction; +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MockCurator; + +import java.util.Optional; + +/** + * Overrides application package fetching, because this part is hard to do without feeding a full app. + * + * @author lulf + * @since 5.1 + */ +public class MockSessionZKClient extends SessionZooKeeperClient { + + private ApplicationPackage app = null; + private Optional<ProvisionInfo> info = null; + private Session.Status sessionStatus; + + public MockSessionZKClient(Curator curator, Path rootPath) { + this(curator, rootPath, (ApplicationPackage)null); + } + + public MockSessionZKClient(Curator curator, Path rootPath, Optional<ProvisionInfo> provisionInfo) { + this(curator, rootPath); + this.info = provisionInfo; + } + + public MockSessionZKClient(Curator curator, Path rootPath, ApplicationPackage application) { + super(curator, rootPath); + this.app = application; + } + + public MockSessionZKClient(ApplicationPackage app) { + super(new MockCurator(), Path.createRoot()); + this.app = app; + } + + @Override + public ApplicationPackage loadApplicationPackage() { + if (app != null) return app; + return new MockApplicationPackage.Builder().withEmptyServices().build(); + } + + @Override + ProvisionInfo getProvisionInfo() { + return info.orElseThrow(() -> new IllegalStateException("Trying to read provision info, but no provision info exists")); + } + + @Override + public Transaction createWriteStatusTransaction(Session.Status status) { + return new DummyTransaction().add((DummyTransaction.RunnableOperation) () -> { + sessionStatus = status; + }); + } + + @Override + public Session.Status readStatus() { + return sessionStatus; + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java new file mode 100644 index 00000000000..9faba599e3a --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java @@ -0,0 +1,89 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.container.jdisc.HttpRequest; + +import org.junit.Test; + +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author musum + */ +public class PrepareParamsTest { + + @Test + public void testCorrectParsing() { + PrepareParams prepareParams = createParams("http://foo:19071/application/v2/", + TenantName.defaultName()); + + assertThat(prepareParams.getApplicationId(), is(ApplicationId.defaultId())); + assertFalse(prepareParams.isDryRun()); + assertFalse(prepareParams.ignoreValidationErrors()); + assertThat(prepareParams.getVespaVersion(), is(Optional.<String>empty())); + assertTrue(prepareParams.getTimeoutBudget().hasTimeLeft()); + assertThat(prepareParams.getRotations().size(), is(0)); + } + + + static final String rotation = "rotation-042.vespa.a02.yahoodns.net"; + static final String vespaVersion = "6.37.49"; + static final String request = "http://foo:19071/application/v2/tenant/foo/application/bar?" + + PrepareParams.DRY_RUN_PARAM_NAME + "=true&" + + PrepareParams.IGNORE_VALIDATION_PARAM_NAME + "=false&" + + PrepareParams.APPLICATION_NAME_PARAM_NAME + "=baz&" + + PrepareParams.VESPA_VERSION_PARAM_NAME + "=" + vespaVersion + "&" + + PrepareParams.DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME+ "=" + vespaVersion; + + @Test + public void testCorrectParsingWithRotation() { + PrepareParams prepareParams = createParams(request + "&" + + PrepareParams.ROTATIONS_PARAM_NAME + "=" + rotation, + TenantName.from("foo")); + + assertThat(prepareParams.getApplicationId().serializedForm(), is("foo:baz:default")); + assertTrue(prepareParams.isDryRun()); + assertFalse(prepareParams.ignoreValidationErrors()); + final Version expectedVersion = Version.fromString(vespaVersion); + assertThat(prepareParams.getVespaVersion().get(), is(expectedVersion)); + assertTrue(prepareParams.getTimeoutBudget().hasTimeLeft()); + final Set<Rotation> rotations = prepareParams.getRotations(); + assertThat(rotations.size(), is(1)); + assertThat(rotations, contains(equalTo(new Rotation(rotation)))); + assertThat(prepareParams.getDockerVespaImageVersion().get(), is(expectedVersion)); + } + + @Test + public void testCorrectParsingWithSeveralRotations() { + final String rotationTwo = "rotation-043.vespa.a02.yahoodns.net"; + final String twoRotations = rotation + "," + rotationTwo; + PrepareParams prepareParams = createParams(request + "&" + + PrepareParams.ROTATIONS_PARAM_NAME + "=" + twoRotations, + TenantName.from("foo")); + final Set<Rotation> rotations = prepareParams.getRotations(); + assertThat(rotations, containsInAnyOrder(new Rotation(rotation), new Rotation(rotationTwo))); + } + + // Create PrepareParams from a request (based on uri and tenant name) + private static PrepareParams createParams(String uri, TenantName tenantName) { + return PrepareParams.fromHttpRequest( + HttpRequest.createTestRequest(uri, + com.yahoo.jdisc.http.HttpRequest.Method.PUT), + tenantName, + new ConfigserverConfig(new ConfigserverConfig.Builder())); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java new file mode 100644 index 00000000000..6d8f93f4f8f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionRepoTest.java @@ -0,0 +1,161 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.text.Utf8; +import com.yahoo.transaction.Transaction; +import com.yahoo.vespa.config.server.*; + +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.curator.Curator; +import org.junit.Before; +import org.junit.Test; + +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.LongPredicate; + +/** + * @author lulf + * @since 5.1 + */ +public class RemoteSessionRepoTest extends TestWithCurator { + + private RemoteSessionRepo remoteSessionRepo; + + @Before + public void setupFacade() throws Exception { + createSession(2l, false); + createSession(3l, false); + curator.create(Path.fromString("/applications")); + curator.create(Path.fromString("/sessions")); + Tenant tenant = TenantBuilder.create(new TestComponentRegistry(curator), TenantName.defaultName(), Path.createRoot()).build(); + this.remoteSessionRepo = tenant.getRemoteSessionRepo(); + } + + private void createSession(long sessionId, boolean wait) { + createSession("", sessionId, wait); + } + + + private void createSession(String root, long sessionId, boolean wait) { + Path rootPath = Path.fromString(root).append("sessions"); + curator.create(rootPath); + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, rootPath.append(String.valueOf(sessionId))); + zkc.createNewSession(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + if (wait) { + Curator.CompletionWaiter waiter = zkc.getUploadWaiter(); + waiter.awaitCompletion(Duration.ofSeconds(120)); + } + } + + @Test + public void testInitialize() { + assertSessionExists(2l); + assertSessionExists(3l); + } + + @Test + public void testCreateSession() throws Exception { + createSession(0l, true); + assertSessionExists(0l); + } + + @Test + public void testSessionStateChange() throws Exception { + Path session = Path.fromString("/sessions/0"); + createSession(0l, true); + assertSessionStatus(0l, Session.Status.NEW); + assertStatusChange(0l, Session.Status.PREPARE); + assertStatusChange(0l, Session.Status.ACTIVATE); + + curator.delete(session); + assertSessionRemoved(0l); + assertNull(remoteSessionRepo.getSession(0l)); + } + + @Test + public void testBadApplicationRepoOnActivate() throws Exception { + ApplicationRepo applicationRepo = new FailingApplicationRepo(); + curator.framework().create().forPath("/mytenant"); + Tenant tenant = TenantBuilder.create(new TestComponentRegistry(curator), TenantName.from("mytenant"), Path.fromString("mytenant")) + .withApplicationRepo(applicationRepo) + .build(); + remoteSessionRepo = tenant.getRemoteSessionRepo(); + createSession("/mytenant", 2l, true); + assertThat(remoteSessionRepo.listSessions().size(), is(1)); + } + + private void assertStatusChange(long sessionId, Session.Status status) throws Exception { + Path statePath = Path.fromString("/sessions/" + sessionId).append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH); + curator.create(statePath); + curatorFramework.setData().forPath(statePath.getAbsolute(), Utf8.toBytes(status.toString())); + System.out.println("Setting status " + status + " for " + sessionId); + assertSessionStatus(0l, status); + } + + private void assertSessionRemoved(long sessionId) { + waitFor(p -> remoteSessionRepo.getSession(sessionId) == null, sessionId); + assertNull(remoteSessionRepo.getSession(sessionId)); + } + + private void assertSessionExists(long sessionId) { + assertSessionStatus(sessionId, Session.Status.NEW); + } + + private void assertSessionStatus(long sessionId, Session.Status status) { + waitFor(p -> remoteSessionRepo.getSession(sessionId) != null && + remoteSessionRepo.getSession(sessionId).getStatus() == status, sessionId); + assertNotNull(remoteSessionRepo.getSession(sessionId)); + assertThat(remoteSessionRepo.getSession(sessionId).getStatus(), is(status)); + } + + private void waitFor(LongPredicate predicate, long sessionId) { + long endTime = System.currentTimeMillis() + 60_000; + boolean ok; + do { + ok = predicate.test(sessionId); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } while (System.currentTimeMillis() < endTime && !ok); + } + + private class FailingApplicationRepo implements ApplicationRepo { + @Override + public List<ApplicationId> listApplications() { + return Collections.singletonList(ApplicationId.defaultId()); + } + + @Override + public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) { + return null; + } + + @Override + public long getSessionIdForApplication(ApplicationId applicationId) { + throw new IllegalArgumentException("Bad id " + applicationId); + } + + @Override + public void deleteApplication(ApplicationId applicationId) { + + } + + @Override + public void close() { + + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java new file mode 100644 index 00000000000..2fc65eb77a8 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java @@ -0,0 +1,284 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.io.Files; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.NullConfigModelRegistry; +import com.yahoo.config.model.api.*; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.Version; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.PathProvider; +import com.yahoo.vespa.config.server.TestComponentRegistry; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.model.VespaModel; +import com.yahoo.vespa.model.VespaModelFactory; + +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class RemoteSessionTest { + + private Curator curator; + private PathProvider pathProvider; + + @Before + public void setupTest() throws Exception { + curator = new MockCurator(); + pathProvider = new PathProvider(Path.createRoot()); + } + + @Test + public void require_that_session_is_initialized() { + Session session = createSession(2); + assertThat(session.getSessionId(), is(2l)); + session = createSession(Long.MAX_VALUE); + assertThat(session.getSessionId(), is(Long.MAX_VALUE)); + } + + @Test + public void require_that_applications_are_loaded() throws IOException, SAXException { + RemoteSession session = createSession(3, Arrays.asList(new MockModelFactory(), new VespaModelFactory(new NullConfigModelRegistry()))); + session.loadPrepared(); + ApplicationSet applicationSet = session.ensureApplicationLoaded(); + assertNotNull(applicationSet); + assertThat(applicationSet.getApplicationGeneration(), is(3l)); + assertThat(applicationSet.getForVersionOrLatest(Optional.empty()).getName(), is("foo")); + assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty()).getModel()); + session.deactivate(); + + applicationSet = session.ensureApplicationLoaded(); + assertNotNull(applicationSet); + assertThat(applicationSet.getApplicationGeneration(), is(3l)); + assertThat(applicationSet.getForVersionOrLatest(Optional.empty()).getName(), is("foo")); + assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty()).getModel()); + } + + @Test(expected = IllegalArgumentException.class) + public void require_that_new_invalid_application_throws_exception() throws IOException, SAXException { + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(1, 2, 0); + failingFactory.throwOnLoad = true; + + MockModelFactory okFactory = new MockModelFactory(); + okFactory.vespaVersion = Version.fromIntValues(1, 1, 0); + okFactory.throwOnLoad = false; + + RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory)); + session.loadPrepared(); + } + + @Test + public void require_that_application_incompatible_with_latestmajor_is_loaded_on_earlier_major() throws IOException, SAXException { + MockModelFactory okFactory1 = new MockModelFactory(); + okFactory1.vespaVersion = Version.fromIntValues(1, 1, 0); + okFactory1.throwOnLoad = false; + + MockModelFactory okFactory2 = new MockModelFactory(); + okFactory2.vespaVersion = Version.fromIntValues(1, 2, 0); + okFactory2.throwOnLoad = false; + + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(2, 0, 0); + failingFactory.throwOnLoad = true; + + RemoteSession session = createSession(3, Arrays.asList(okFactory1, failingFactory, okFactory2)); + session.loadPrepared(); + } + + @Test + public void require_that_old_invalid_application_does_not_throw_exception_if_skipped() throws IOException, SAXException { + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(1, 1, 0); + failingFactory.throwOnLoad = true; + + MockModelFactory okFactory = + new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>"); + okFactory.vespaVersion = Version.fromIntValues(1, 2, 0); + okFactory.throwOnLoad = false; + + RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory)); + session.loadPrepared(); + } + + @Test + public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_across_major_versions() throws IOException, SAXException { + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(1, 0, 0); + failingFactory.throwOnLoad = true; + + MockModelFactory okFactory = + new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>"); + okFactory.vespaVersion = Version.fromIntValues(2, 0, 0); + okFactory.throwOnLoad = false; + + RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory)); + session.loadPrepared(); + } + + @Test + public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_when_new_major_is_incompatible() throws IOException, SAXException { + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(1, 0, 0); + failingFactory.throwOnLoad = true; + + MockModelFactory okFactory = + new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>"); + okFactory.vespaVersion = Version.fromIntValues(1, 1, 0); + okFactory.throwOnLoad = false; + + MockModelFactory tooNewFactory = + new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>"); + tooNewFactory.vespaVersion = Version.fromIntValues(2, 0, 0); + tooNewFactory.throwOnLoad = true; + + RemoteSession session = createSession(3, Arrays.asList(tooNewFactory, okFactory, failingFactory)); + session.loadPrepared(); + } + + @Test + public void require_that_an_application_package_can_limit_to_one_major_version() throws IOException, SAXException { + ApplicationPackage application = + new MockApplicationPackage.Builder().withServices("<services major-version='2' version=\"1.0\"></services>").build(); + + MockModelFactory failingFactory = new MockModelFactory(); + failingFactory.vespaVersion = Version.fromIntValues(3, 0, 0); + failingFactory.throwOnLoad = true; + + MockModelFactory okFactory = new MockModelFactory(); + okFactory.vespaVersion = Version.fromIntValues(2, 0, 0); + okFactory.throwOnLoad = false; + + SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(3), application); + RemoteSession session = createSession(3, zkc, Arrays.asList(okFactory, failingFactory)); + session.loadPrepared(); + + // Does not cause an exception because model version 3 is skipped + } + + @Test + public void require_that_session_status_is_updated() throws IOException, SAXException { + SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(3)); + RemoteSession session = createSession(3, zkc); + assertThat(session.getStatus(), is(Session.Status.NEW)); + zkc.writeStatus(Session.Status.PREPARE); + assertThat(session.getStatus(), is(Session.Status.PREPARE)); + } + + @Test + public void require_that_permanent_app_is_used() { + Optional<PermanentApplicationPackage> permanentApp = Optional.of(new PermanentApplicationPackage( + new ConfigserverConfig(new ConfigserverConfig.Builder().applicationDirectory(Files.createTempDir().getAbsolutePath())))); + MockModelFactory mockModelFactory = new MockModelFactory(); + try { + int sessionId = 3; + SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(sessionId)); + createSession(sessionId, zkc, Collections.singletonList(mockModelFactory), permanentApp).ensureApplicationLoaded(); + } catch (Exception e) { + e.printStackTrace(); + // ignore, we're not interested in deploy errors as long as the below state is OK. + } + assertNotNull(mockModelFactory.modelContext); + assertTrue(mockModelFactory.modelContext.permanentApplicationPackage().isPresent()); + } + + private RemoteSession createSession(long sessionId) { + return createSession(sessionId, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))); + } + private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc) { + return createSession(sessionId, zkc, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry()))); + } + private RemoteSession createSession(long sessionId, List<ModelFactory> modelFactories) { + SessionZooKeeperClient zkc = new MockSessionZKClient(curator, pathProvider.getSessionDir(sessionId)); + return createSession(sessionId, zkc, modelFactories); + } + + private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories) { + return createSession(sessionId, zkc, modelFactories, Optional.empty()); + } + + private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories, Optional<PermanentApplicationPackage> permanentApplicationPackage) { + zkc.writeStatus(Session.Status.NEW); + zkc.writeApplicationId(new ApplicationId.Builder().applicationName("foo").instanceName("bim").build()); + return new RemoteSession(TenantName.from("default"), sessionId, new TestComponentRegistry(curator, new ModelFactoryRegistry(modelFactories), permanentApplicationPackage), zkc); + } + + private class MockModelFactory implements ModelFactory { + + public boolean throwOnLoad = false; + public ModelContext modelContext; + public Version vespaVersion = Version.fromIntValues(1, 2, 3); + + /** The validation overrides of this, or null if none */ + private final String validationOverrides; + + public MockModelFactory() { this(null); } + + public MockModelFactory(String validationOverrides) { + this.validationOverrides = validationOverrides; + } + + @Override + public Version getVersion() { + return vespaVersion; + } + + @Override + public Model createModel(ModelContext modelContext) { + if (throwOnLoad) { + throw new IllegalArgumentException("Foo"); + } + this.modelContext = modelContext; + return loadModel(); + } + + public Model loadModel() { + try { + Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(); + ApplicationPackage application = new MockApplicationPackage.Builder().withEmptyHosts().withEmptyServices().withValidationOverrides(validationOverrides).build(); + DeployState deployState = new DeployState.Builder().applicationPackage(application).now(now).build(); + return new VespaModel(deployState); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) { + if (throwOnLoad) { + throw new IllegalArgumentException("Foo"); + } + this.modelContext = modelContext; + return new ModelCreateResult(loadModel(), new ArrayList<>()); + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java new file mode 100644 index 00000000000..83ed65f03e4 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionFactoryTest.java @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.io.Files; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.*; +import com.yahoo.vespa.config.server.http.SessionCreate; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class SessionFactoryTest extends TestWithTenant { + private SessionFactory factory; + + @Before + public void setup_test() throws Exception { + factory = tenant.getSessionFactory(); + } + + @Test + public void require_that_session_can_be_created() throws IOException { + LocalSession session = getLocalSession(); + assertNotNull(session); + assertThat(session.getSessionId(), is(2l)); + assertTrue(session.getCreateTime() > 0); + } + + @Test + public void require_that_application_name_is_set_in_application_package() throws IOException, JSONException { + LocalSession session = getLocalSession("book"); + assertNotNull(session); + ApplicationFile meta = session.getApplicationFile(Path.createRoot().append(".applicationMetaData"), LocalSession.Mode.READ); + assertTrue(meta.exists()); + JSONObject json = new JSONObject(IOUtils.readAll(meta.createReader())); + assertThat(json.getJSONObject("application").getString("name"), is("book")); + } + + @Test + public void require_that_session_can_be_created_from_existing() throws IOException { + LocalSession session = getLocalSession(); + assertNotNull(session); + assertThat(session.getSessionId(), is(2l)); + LocalSession session2 = factory.createSessionFromExisting(session, new BaseDeployLogger(), TimeoutBudgetTest.day()); + assertNotNull(session2); + assertThat(session2.getSessionId(), is(3l)); + } + + @Test(expected = RuntimeException.class) + public void require_that_invalid_app_dir_is_handled() throws IOException { + factory.createSession(new File("doesnotpointtoavaliddir"), "music", new BaseDeployLogger(), TimeoutBudgetTest.day()); + } + + private LocalSession getLocalSession() throws IOException { + return getLocalSession("music"); + } + + private LocalSession getLocalSession(String appName) throws IOException { + CompressedApplicationInputStream app = CompressedApplicationInputStream.createFromCompressedStream(new FileInputStream(CompressedApplicationInputStreamTest.createTarFile()), SessionCreate.APPLICATION_X_GZIP); + return factory.createSession(app.decompress(Files.createTempDir()), appName, new BaseDeployLogger(), TimeoutBudgetTest.day()); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java new file mode 100644 index 00000000000..308e6aa29d2 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionPreparerTest.java @@ -0,0 +1,286 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.collect.ImmutableSet; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelCreateResult; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.*; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.MemoryApplicationRepo; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.configchange.MockRestartAction; +import com.yahoo.vespa.config.server.configchange.RestartActions; +import com.yahoo.vespa.config.server.http.InvalidApplicationException; +import com.yahoo.vespa.config.server.model.TestModelFactory; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; + +/** + * @author lulf + * @since 5.1 + */ +public class SessionPreparerTest extends TestWithCurator { + + private static final Path appPath = Path.createRoot().append("testapp"); + private static final File testApp = new File("src/test/apps/app"); + private static final File invalidTestApp = new File("src/test/apps/illegalApp"); + + private SessionPreparer preparer; + private TestComponentRegistry componentRegistry; + private MockFileDistributionFactory fileDistributionFactory; + private Path tenantPath = appPath; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + componentRegistry = new TestComponentRegistry(curator); + fileDistributionFactory = (MockFileDistributionFactory)componentRegistry.getFileDistributionFactory(); + preparer = createPreparer(); + } + + private SessionPreparer createPreparer() { + return createPreparer(HostProvisionerProvider.empty()); + } + + private SessionPreparer createPreparer(HostProvisionerProvider hostProvisionerProvider) { + ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList( + new TestModelFactory(Version.fromIntValues(1, 2, 3)), + new TestModelFactory(Version.fromIntValues(3, 2, 1)))); + return createPreparer(modelFactoryRegistry, hostProvisionerProvider); + } + + private SessionPreparer createPreparer(ModelFactoryRegistry modelFactoryRegistry, + HostProvisionerProvider hostProvisionerProvider) { + return new SessionPreparer( + modelFactoryRegistry, + componentRegistry.getFileDistributionFactory(), + hostProvisionerProvider, + new PermanentApplicationPackage(componentRegistry.getConfigserverConfig()), + componentRegistry.getConfigserverConfig(), + componentRegistry.getConfigDefinitionRepo(), + curator, + componentRegistry.getZone()); + } + + @Test(expected = InvalidApplicationException.class) + public void require_that_application_validation_exception_is_not_caught() throws IOException, SAXException { + FilesApplicationPackage app = getApplicationPackage(invalidTestApp); + preparer.prepare(getContext(app), getLogger(), new PrepareParams(), Optional.empty(), tenantPath); + } + + @Test + public void require_that_application_validation_exception_is_ignored_if_forced() throws IOException, SAXException { + FilesApplicationPackage app = getApplicationPackage(invalidTestApp); + preparer.prepare(getContext(app), getLogger(), new PrepareParams().ignoreValidationErrors(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath); + } + + @Test + public void require_that_zookeeper_is_not_written_to_if_dryrun() throws IOException { + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams().dryRun(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath); + assertFalse(configCurator.exists(appPath.append(ConfigCurator.USERAPP_ZK_SUBPATH).append("services.xml").getAbsolute())); + } + + @Test + public void require_that_filedistribution_is_ignored_on_dryrun() throws IOException { + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams().dryRun(true).timeoutBudget(TimeoutBudgetTest.day()), Optional.empty(), tenantPath); + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().sendDeployedFilesCalled, is(0)); + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().limitSendingOfDeployedFilesToCalled, is(0)); + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().reloadDeployFileDistributorCalled, is(0)); + } + + @Test + public void require_that_application_is_prepared() throws Exception { + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath); + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().sendDeployedFilesCalled, is(2)); + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().limitSendingOfDeployedFilesToCalled, is(2)); + // Should be called only once no matter how many model versions are built + assertThat(fileDistributionFactory.mockFileDistributionProvider.getMockFileDBHandler().reloadDeployFileDistributorCalled, is(1)); + assertTrue(configCurator.exists(appPath.append(ConfigCurator.USERAPP_ZK_SUBPATH).append("services.xml").getAbsolute())); + } + + @Test + public void require_that_prepare_succeeds_if_newer_version_fails() throws IOException { + ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList( + new TestModelFactory(Version.fromIntValues(1, 2, 3)), + new FailingModelFactory(Version.fromIntValues(3, 2, 1), new IllegalArgumentException("BOOHOO")))); + preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty()); + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath); + } + + @Test(expected = InvalidApplicationException.class) + public void require_that_prepare_fails_if_older_version_fails() throws IOException { + ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList( + new TestModelFactory(Version.fromIntValues(3, 2, 3)), + new FailingModelFactory(Version.fromIntValues(1, 2, 1), new IllegalArgumentException("BOOHOO")))); + preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty()); + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath); + } + + @Test(expected = InvalidApplicationException.class) + public void require_exception_for_overlapping_host() throws IOException { + SessionContext ctx = getContext(getApplicationPackage(testApp)); + ((HostRegistry<ApplicationId>)ctx.getHostValidator()).update(applicationId("foo"), Collections.singletonList("mytesthost")); + preparer.prepare(ctx, new BaseDeployLogger(), new PrepareParams(), Optional.empty(), tenantPath); + } + + @Test + public void require_no_warning_for_overlapping_host_for_same_appid() throws IOException { + SessionContext ctx = getContext(getApplicationPackage(testApp)); + ((HostRegistry<ApplicationId>)ctx.getHostValidator()).update(applicationId("default"), Collections.singletonList("mytesthost")); + final StringBuilder logged = new StringBuilder(); + DeployLogger logger = (level, message) -> { + System.out.println(level + ": "+message); + if (level.equals(LogLevel.WARNING) && message.contains("The host mytesthost is already in use")) logged.append("ok"); + }; + preparer.prepare(ctx, logger, new PrepareParams(), Optional.empty(), tenantPath); + assertEquals(logged.toString(), ""); + } + + @Test + public void require_that_application_id_is_written_in_prepare() throws IOException { + TenantName tenant = TenantName.from("tenant"); + ApplicationId origId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName("foo").instanceName("quux").build(); + PrepareParams params = new PrepareParams().applicationId(origId); + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), params, Optional.empty(), tenantPath); + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, appPath); + assertTrue(configCurator.exists(appPath.append(SessionZooKeeperClient.APPLICATION_ID_PATH).getAbsolute())); + assertThat(zkc.readApplicationId(tenant), is(origId)); + } + + @Test + public void require_that_config_change_actions_are_collected_from_all_models() throws IOException { + ServiceInfo service = new ServiceInfo("serviceName", "serviceType", null, new HashMap<>(), "configId", "hostName"); + ModelFactoryRegistry modelFactoryRegistry = new ModelFactoryRegistry(Arrays.asList( + new ConfigChangeActionsModelFactory(Version.fromIntValues(1, 2, 3), + new MockRestartAction("change", Arrays.asList(service))), + new ConfigChangeActionsModelFactory(Version.fromIntValues(1, 2, 4), + new MockRestartAction("other change", Arrays.asList(service))))); + preparer = createPreparer(modelFactoryRegistry, HostProvisionerProvider.empty()); + List<RestartActions.Entry> actions = + preparer.prepare(getContext(getApplicationPackage(testApp)), getLogger(), new PrepareParams(), Optional.empty(), tenantPath). + getRestartActions().getEntries(); + assertThat(actions.size(), is(1)); + assertThat(actions.get(0).getMessages(), equalTo(ImmutableSet.of("change", "other change"))); + } + + private Set<Rotation> readRotationsFromZK(ApplicationId applicationId) { + return new RotationsCache(curator, tenantPath).readRotationsFromZooKeeper(applicationId); + } + + @Test + public void require_that_rotations_are_written_in_prepare() throws IOException { + final String rotations = "mediasearch.msbe.global.vespa.yahooapis.com"; + final ApplicationId applicationId = applicationId("test"); + PrepareParams params = new PrepareParams().applicationId(applicationId).rotations(rotations); + File app = new File("src/test/resources/deploy/app"); + preparer.prepare(getContext(getApplicationPackage(app)), getLogger(), params, Optional.empty(), tenantPath); + assertThat(readRotationsFromZK(applicationId), contains(new Rotation(rotations))); + } + + @Test + public void require_that_rotations_are_read_from_zookeeper_and_used() throws IOException { + final Version vespaVersion = Version.fromIntValues(1, 2, 3); + final TestModelFactory modelFactory = new TestModelFactory(vespaVersion); + preparer = createPreparer(new ModelFactoryRegistry(Arrays.asList(modelFactory)), + HostProvisionerProvider.empty()); + + final String rotations = "foo.msbe.global.vespa.yahooapis.com"; + final ApplicationId applicationId = applicationId("test"); + new RotationsCache(curator, tenantPath).writeRotationsToZooKeeper(applicationId, Collections.singleton(new Rotation(rotations))); + final PrepareParams params = new PrepareParams().applicationId(applicationId); + final File app = new File("src/test/resources/deploy/app"); + preparer.prepare(getContext(getApplicationPackage(app)), getLogger(), params, Optional.empty(), tenantPath); + + // check that the rotation from zookeeper were used + final ModelContext modelContext = modelFactory.getModelContext(); + final Set<Rotation> rotationSet = modelContext.properties().rotations(); + assertThat(rotationSet, contains(new Rotation(rotations))); + + // Check that the persisted value is still the same + assertThat(readRotationsFromZK(applicationId), contains(new Rotation(rotations))); + } + + private SessionContext getContext(FilesApplicationPackage app) throws IOException { + return new SessionContext(app, new SessionZooKeeperClient(curator, appPath), app.getAppDir(), new MemoryApplicationRepo(), new HostRegistry<>(), new SuperModelGenerationCounter(curator)); + } + + private FilesApplicationPackage getApplicationPackage(File testFile) throws IOException { + File appDir = folder.newFolder(); + IOUtils.copyDirectory(testFile, appDir); + return FilesApplicationPackage.fromFile(appDir); + } + + DeployHandlerLogger getLogger() { + return getLogger(false); + } + + DeployHandlerLogger getLogger(boolean verbose) { + return new DeployHandlerLogger(new Slime().get(), verbose, + new ApplicationId.Builder().tenant("testtenant").applicationName("testapp").build()); + } + + private static class FailingModelFactory extends TestModelFactory { + private final RuntimeException exception; + public FailingModelFactory(Version vespaVersion, RuntimeException exception) { + super(vespaVersion); + this.exception = exception; + } + + @Override + public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) { + throw exception; + } + } + + private ApplicationId applicationId(String applicationName) { + return ApplicationId.from(TenantName.defaultName(), + ApplicationName.from(applicationName), InstanceName.defaultName()); + } + + private static class ConfigChangeActionsModelFactory extends TestModelFactory { + private final ConfigChangeAction action; + public ConfigChangeActionsModelFactory(Version vespaVersion, ConfigChangeAction action) { + super(vespaVersion); + this.action = action; + } + + @Override + public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) { + ModelCreateResult result = super.createAndValidateModel(modelContext, ignoreValidationErrors); + return new ModelCreateResult(result.getModel(), Arrays.asList(action)); + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java new file mode 100644 index 00000000000..8549902faf0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepoTest.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import org.junit.Test; + +import com.yahoo.config.provision.TenantName; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +/** + * @author musum + * @since 5.1.14 + */ +public class SessionRepoTest { + @Test + public void require_that_sessionrepo_is_initialized() { + SessionRepo<TestSession> sessionRepo = new SessionRepo<>(); + assertNull(sessionRepo.getSession(1L)); + sessionRepo.addSession(new TestSession(1)); + assertThat(sessionRepo.getSession(1L).getSessionId(), is(1l)); + } + + @Test(expected = IllegalArgumentException.class) + public void require_that_adding_existing_session_fails() { + SessionRepo<TestSession> sessionRepo = new SessionRepo<>(); + final TestSession session = new TestSession(1); + sessionRepo.addSession(session); + sessionRepo.addSession(session); + } + + private class TestSession extends Session { + public TestSession(long sessionId) { + super(TenantName.from("default"), sessionId); + } + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java new file mode 100644 index 00000000000..de64db1bbc9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionTest.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.curator.mock.MockCurator; + +import java.util.ArrayList; +import java.util.Optional; + +/** + * @author lulf + * @since 5.1 + */ +public class SessionTest { + + public static class MockSessionPreparer extends SessionPreparer { + public boolean isPrepared = false; + + public MockSessionPreparer() { + super(null, null, null, null, null, null, new MockCurator(), null); + } + + @Override + public ConfigChangeActions prepare(SessionContext context, DeployLogger logger, PrepareParams params, Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) { + isPrepared = true; + return new ConfigChangeActions(new ArrayList<>()); + } + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java new file mode 100644 index 00000000000..4508d8c234f --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClientTest.java @@ -0,0 +1,118 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.TestWithCurator; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class SessionZooKeeperClientTest extends TestWithCurator { + + @Test + public void require_that_status_can_be_updated() { + SessionZooKeeperClient zkc = createSessionZKClient("1"); + zkc.writeStatus(Session.Status.NEW); + assertThat(zkc.readStatus(), is(Session.Status.NEW)); + + zkc.writeStatus(Session.Status.PREPARE); + assertThat(zkc.readStatus(), is(Session.Status.PREPARE)); + + zkc.writeStatus(Session.Status.ACTIVATE); + assertThat(zkc.readStatus(), is(Session.Status.ACTIVATE)); + + zkc.writeStatus(Session.Status.DEACTIVATE); + assertThat(zkc.readStatus(), is(Session.Status.DEACTIVATE)); + } + + @Test + public void require_that_status_is_written_to_zk() { + SessionZooKeeperClient zkc = createSessionZKClient("2"); + zkc.writeStatus(Session.Status.NEW); + String path = "/2" + ConfigCurator.SESSIONSTATE_ZK_SUBPATH; + assertTrue(configCurator.exists(path)); + assertThat(configCurator.getData(path), is("NEW")); + } + + @Test + public void require_that_status_is_read_from_zk() { + SessionZooKeeperClient zkc = createSessionZKClient("3"); + curator.set(Path.fromString("3").append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH), Utf8.toBytes("PREPARE")); + assertThat(zkc.readStatus(), is(Session.Status.PREPARE)); + } + + @Test + public void require_that_application_id_is_written_to_zk() { + ApplicationId id = new ApplicationId.Builder() + .tenant("tenant") + .applicationName("foo").instanceName("bim").build(); + SessionZooKeeperClient zkc = createSessionZKClient("3"); + zkc.writeApplicationId(id); + String path = "/3/" + SessionZooKeeperClient.APPLICATION_ID_PATH; + assertTrue(configCurator.exists(path)); + assertThat(configCurator.getData(path), is("tenant:foo:bim")); + } + + @Test + public void require_that_application_id_is_read_from_zk() { + ApplicationId id = new ApplicationId.Builder() + .tenant("tenant") + .applicationName("bar").instanceName("quux").build(); + String idNoVersion = id.serializedForm(); + assertApplicationIdParse("3", idNoVersion, idNoVersion); + } + + @Test + public void require_that_default_name_is_returned_if_node_does_not_exist() { + assertThat(createSessionZKClient("3").readApplicationId(TenantName.defaultName()).application().value(), is("default")); + } + + @Test + public void require_that_create_time_can_be_written_and_read() { + SessionZooKeeperClient zkc = createSessionZKClient("3"); + curator.delete(Path.fromString("3")); + assertThat(zkc.readCreateTime(), is(0l)); + zkc.createNewSession(123456l, TimeUnit.SECONDS); + assertThat(zkc.readCreateTime(), is(123456l)); + } + + @Test + public void require_that_create_time_has_correct_unit() { + SessionZooKeeperClient zkc = createSessionZKClient("3"); + curator.delete(Path.fromString("3")); + assertThat(zkc.readCreateTime(), is(0l)); + zkc.createNewSession(60, TimeUnit.MINUTES); + assertThat(zkc.readCreateTime(), is(3600l)); + } + + private void assertApplicationIdParse(String sessionId, String idString, String expectedIdString) { + SessionZooKeeperClient zkc = createSessionZKClient(sessionId); + String path = "/" + sessionId + "/" + SessionZooKeeperClient.APPLICATION_ID_PATH; + configCurator.putData(path, idString); + ApplicationId zkId = zkc.readApplicationId(TenantName.defaultName()); + assertThat(zkId.serializedForm(), is(expectedIdString)); + } + + private SessionZooKeeperClient createSessionZKClient(String generation) { + return createSessionZKClient(generation, 100); + } + + private SessionZooKeeperClient createSessionZKClient(String generation, long createTimeInMillis) { + SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator, Path.fromString(generation)); + zkc.createNewSession(createTimeInMillis, TimeUnit.MILLISECONDS); + return zkc; + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg new file mode 100644 index 00000000000..0bc17bae65e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/a.cfg @@ -0,0 +1,18 @@ +asyncfetchocc 10 +e 4 +search[2].feeder[1] "bazfeeder" +search[1].feeder[0] "barfeeder1_1" +search[1].feeder[3] "barfeeder2_1" +onlyindef 45 + +speciallog[0].filehandler.rotation "0 1 ..." + +rulebase[4] +rulebase[0].name "cjk" +rulebase[0].rules "# Use unicode equivalents in java source:\n#\n# 佳:\u4f73\n# 能:\u80fd\n# 索:\u7d22\n# 尼:\u5c3c\n# 惠:\u60e0\n# 普:\u666e\n\n@default\n\na索 -> 索a;\n\n[brand] -> brand:[brand];\n\n[brand] :- 索尼,惠普,佳能;\n" +rulebase[1].name "common" +rulebase[1].rules "## Some test rules\n\n# Spelling correction\nbahc -> bach;\n\n# Stopwords\nsomelongstopword -> ;\n[stopword] -> ;\n[stopword] :- someotherlongstopword, yetanotherstopword;\n\n# \n[song] by [artist] -> song:[song] artist:[artist];\n\n[song] :- together, imagine, tinseltown;\n[artist] :- youngbloods, beatles, zappa;\n\n# Negative\nvarious +> -kingz;\n\n\n" +rulebase[2].name "egyik" +rulebase[2].rules "@include(common.sr)\n@automata(/home/vespa/etc/vespa/fsa/stopwords.fsa)\n[stopwords] -> ;\n\n" +rulebase[3].name "masik" +rulebase[3].rules "@include(common.sr)\n[stopwords] :- etaoin, shrdlu;\n[stopwords] -> ;\n\n" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg new file mode 100644 index 00000000000..88b50384058 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/b.search#cluster.sports#c0#r0#indexer4.cfg @@ -0,0 +1 @@ +usercfgwithid 86 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg new file mode 100644 index 00000000000..b34c4ed311e --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/c.cfg @@ -0,0 +1 @@ +foo "test" diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg new file mode 100644 index 00000000000..12c5b53de7d --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/d.cfg @@ -0,0 +1 @@ +theint 34 diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg new file mode 100644 index 00000000000..73ed41667f9 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/userconfigs/spooler.cfg @@ -0,0 +1 @@ +keepsuccess true diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java new file mode 100644 index 00000000000..f517145fd91 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/version/VersionStateTest.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.version; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + */ +public class VersionStateTest { + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + + @Test + public void upgrade() throws IOException { + Version unknownVersion = Version.fromIntValues(0, 0, 0); + File versionFile = tempDir.newFile(); + VersionState state = new VersionState(versionFile); + assertThat(state.storedVersion(), is(unknownVersion)); + assertTrue(state.isUpgraded()); + state.saveNewVersion(); + assertFalse(state.isUpgraded()); + + IOUtils.writeFile(versionFile, "badversion", false); + assertThat(state.storedVersion(), is(unknownVersion)); + assertTrue(state.isUpgraded()); + + IOUtils.writeFile(versionFile, "5.0.0", false); + assertThat(state.storedVersion(), is(Version.fromIntValues(5, 0, 0))); + assertTrue(state.isUpgraded()); + + state.saveNewVersion(); + assertThat(state.currentVersion(), is(state.storedVersion())); + assertFalse(state.isUpgraded()); + } + + @Test + public void serverdbfile() throws IOException { + File dbDir = tempDir.newFolder(); + VersionState state = new VersionState(new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(dbDir.getAbsolutePath()))); + state.saveNewVersion(); + File versionFile = new File(dbDir, "vespa_version"); + assertTrue(versionFile.exists()); + Version stored = Version.fromString(IOUtils.readFile(versionFile)); + assertThat(stored, is(state.currentVersion())); + } +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java new file mode 100644 index 00000000000..b370b148fe0 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ConfigCuratorTest.java @@ -0,0 +1,239 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.text.Utf8; +import com.yahoo.vespa.curator.mock.MockCurator; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests the ZKFacade using a curator mock. + * + * @author <a href="musum@yahoo-inc.com">Harald Musum</a> + */ +public class ConfigCuratorTest { + + private final String defKey1 = "attributes"; + + private final String payload1 = "attribute[5]\n" + + "attribute[0].name Popularity\n" + + "attribute[0].datatype string\n" + + "attribute[0].collectiontype single\n" + + "attribute[0].removeifzero false\n" + + "attribute[0].createifnonexistent false\n" + + "attribute[0].loadtype \"always\"\n" + + "attribute[0].uniqueonly false\n" + + "attribute[0].sparse false\n" + + "attribute[0].noupdate false\n" + + "attribute[0].fastsearch false\n" + + "attribute[0].fastaggregate false\n" + + "attribute[0].fastersearch false\n" + + "attribute[1].name atA\n" + + "attribute[1].datatype string\n" + + "attribute[1].collectiontype weightedset\n" + + "attribute[1].removeifzero false\n" + + "attribute[1].createifnonexistent false\n" + + "attribute[1].loadtype \"always\"\n" + + "attribute[1].uniqueonly false\n" + + "attribute[1].sparse false\n" + + "attribute[1].noupdate false\n" + + "attribute[1].fastsearch true\n" + + "attribute[1].fastaggregate false\n" + + "attribute[1].fastersearch false\n" + + "attribute[2].name default_fieldlength\n" + + "attribute[2].datatype uint32\n" + + "attribute[2].collectiontype single\n" + + "attribute[2].removeifzero false\n" + + "attribute[2].createifnonexistent false\n" + + "attribute[2].loadtype \"always\"\n" + + "attribute[2].uniqueonly false\n" + + "attribute[2].sparse false\n" + + "attribute[2].noupdate true\n" + + "attribute[2].fastsearch false\n" + + "attribute[2].fastaggregate false\n" + + "attribute[2].fastersearch false\n" + + "attribute[3].name default_literal_fieldlength\n" + + "attribute[3].datatype uint32\n" + + "attribute[3].collectiontype single\n" + + "attribute[3].removeifzero false\n" + + "attribute[3].createifnonexistent false\n" + + "attribute[3].loadtype \"always\"\n" + + "attribute[3].uniqueonly false\n" + + "attribute[3].sparse false\n" + + "attribute[3].noupdate true\n" + + "attribute[3].fastsearch false\n" + + "attribute[3].fastaggregate false\n" + + "attribute[3].fastersearch false\n" + + "attribute[4].name artist_fieldlength\n" + + "attribute[4].datatype uint32\n" + + "attribute[4].collectiontype single\n" + + "attribute[4].removeifzero false\n" + + "attribute[4].createifnonexistent false\n" + + "attribute[4].loadtype \"always\"\n" + + "attribute[4].uniqueonly false\n" + + "attribute[4].sparse false\n" + + "attribute[4].noupdate true\n" + + "attribute[4].fastsearch false\n" + + "attribute[4].fastaggregate false\n" + + "attribute[4].fastersearch false\n"; + + private final String payload3 = "attribute[5]\n" + + "attribute[0].name Popularity\n" + + "attribute[0].datatype String\n" + + "attribute[0].collectiontype single\n" + + "attribute[0].removeifzero false\n" + + "attribute[0].createifnonexistent false\n" + + "attribute[0].loadtype \"always\"\n" + + "attribute[0].uniqueonly false\n" + + "attribute[0].sparse false\n" + + "attribute[0].noupdate false\n" + + "attribute[0].fastsearch false\n" + + "attribute[0].fastaggregate false\n" + + "attribute[0].fastersearch false\n" + + "attribute[1].name atA\n" + + "attribute[1].datatype string\n" + + "attribute[1].collectiontype weightedset\n" + + "attribute[1].removeifzero false\n" + + "attribute[1].createifnonexistent false\n" + + "attribute[1].loadtype \"always\"\n" + + "attribute[1].uniqueonly false\n" + + "attribute[1].sparse false\n" + + "attribute[1].noupdate false\n" + + "attribute[1].fastsearch true\n" + + "attribute[1].fastaggregate false\n" + + "attribute[1].fastersearch false\n" + + "attribute[2].name default_fieldlength\n" + + "attribute[2].datatype uint32\n" + + "attribute[2].collectiontype single\n" + + "attribute[2].removeifzero false\n" + + "attribute[2].createifnonexistent false\n" + + "attribute[2].loadtype \"always\"\n" + + "attribute[2].uniqueonly false\n" + + "attribute[2].sparse false\n" + + "attribute[2].noupdate true\n" + + "attribute[2].fastsearch false\n" + + "attribute[2].fastaggregate false\n" + + "attribute[2].fastersearch false\n" + + "attribute[3].name default_literal_fieldlength\n" + + "attribute[3].datatype uint32\n" + + "attribute[3].collectiontype single\n" + + "attribute[3].removeifzero false\n" + + "attribute[3].createifnonexistent false\n" + + "attribute[3].loadtype \"always\"\n" + + "attribute[3].uniqueonly false\n" + + "attribute[3].sparse false\n" + + "attribute[3].noupdate true\n" + + "attribute[3].fastsearch false\n" + + "attribute[3].fastaggregate false\n" + + "attribute[3].fastersearch false\n" + + "attribute[4].name artist_fieldlength\n" + + "attribute[4].datatype uint32\n" + + "attribute[4].collectiontype single\n" + + "attribute[4].removeifzero false\n" + + "attribute[4].createifnonexistent false\n" + + "attribute[4].loadtype \"always\"\n" + + "attribute[4].uniqueonly false\n" + + "attribute[4].sparse false\n" + + "attribute[4].noupdate true\n" + + "attribute[4].fastsearch false\n" + + "attribute[4].fastaggregate false\n" + + "attribute[4].fastersearch false\n"; + + private void initAndClearZK(ConfigCurator zkIf) { + zkIf.initAndClear(ConfigCurator.DEFCONFIGS_ZK_SUBPATH); + zkIf.initAndClear(ConfigCurator.USERAPP_ZK_SUBPATH); + } + + private ConfigCurator deployApp() { + ConfigCurator zkIf = getFacade(); + initAndClearZK(zkIf); + zkIf.putData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, defKey1, payload1); + // zkIf.putData(ConfigCurator.USERCONFIGS_ZK_SUBPATH, cfgKey1, payload3); + String partitionsDef = "version=7\\n" + + "dataset[].id int\\n" + + "dataset[].partbits int default=6"; + zkIf.putData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, "partitions", partitionsDef); + String partitionsUser = "dataset[0].partbits 8\\n"; + // zkIf.putData(ConfigCurator.USERCONFIGS_ZK_SUBPATH, "partitions", partitionsUser); + return zkIf; + } + + @Test + public void testZKInterface() { + ConfigCurator zkIf = getFacade(); + zkIf.putData("", "test", "foo"); + zkIf.putData("/test", "me", "bar"); + zkIf.putData("", "test;me;now,then", "baz"); + assertEquals(zkIf.getData("", "test"), "foo"); + assertEquals(zkIf.getData("/test", "me"), "bar"); + assertEquals(zkIf.getData("", "test;me;now,then"), "baz"); + } + + @Test + public void testWatcher() { + ConfigCurator zkIf = getFacade(); + + String data = zkIf.getData("/nothere"); + assertNull(data); + zkIf.putData("", "/nothere", "foo"); + assertEquals(zkIf.getData("/nothere"), "foo"); + + zkIf.putData("", "test", "foo"); + data = zkIf.getData("/test"); + assertEquals(data, "foo"); + zkIf.putData("", "/test", "bar"); + data = zkIf.getData("/test"); + assertEquals(data, "bar"); + + zkIf.getChildren("/"); + zkIf.putData("", "test2", "foo2"); + } + + private ConfigCurator getFacade() { + return ConfigCurator.create(new MockCurator()); + } + + @Test + public void testGetDeployedData() { + ConfigCurator zkIf = deployApp(); + assertEquals(zkIf.getData(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, defKey1), payload1); + } + + @Test + public void testEmptyData() { + ConfigCurator zkIf = getFacade(); + zkIf.createNode("/empty", "data"); + assertEquals("", zkIf.getData("/empty", "data")); + } + + @Test + public void testRecursiveDelete() { + ConfigCurator configCurator = getFacade(); + configCurator.putData("/foo", Utf8.toBytes("sadsdfsdfsdfsdf")); + configCurator.putData("/foo/bar", Utf8.toBytes("dsfsdffds")); + configCurator.putData("/foo/baz", + Utf8.toBytes("sdf\u00F8l ksdfl skdflsk dflsdkfd welkr3k lkr e4kt4 54l4l353k l534klk3lk4l33k5l 353l4k l43k l4k")); + configCurator.putData("/foo/bar/dill", Utf8.toBytes("sdfsfe 23 42 3 3 2342")); + configCurator.putData("/foo", Utf8.toBytes("sdcfsdfsdf")); + configCurator.putData("/foo", Utf8.toBytes("sdcfsd sdfdffsdf")); + configCurator.deleteRecurse("/foo"); + assertFalse(configCurator.exists("/foo")); + assertFalse(configCurator.exists("/foo/bar")); + assertFalse(configCurator.exists("/foo/bar/dill")); + assertFalse(configCurator.exists("/foo/bar/baz")); + try { + configCurator.getChildren("/foo"); + fail("Got children from nonexisting ZK path"); + } catch (RuntimeException e) { + assertTrue(e.getCause().getMessage().matches(".*NoNode.*")); + } + configCurator.deleteRecurse("/nonexisting"); + } + + @Test + public void testGetZkNodePath() { + assertEquals("foo,1", ConfigCurator.getZkNodePath(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, "foo", "1", "a/b")); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java new file mode 100644 index 00000000000..95d17156e63 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounterTest.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.vespa.config.server.TestWithCurator; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author lulf + * @since 5.1 + */ +public class InitializedCounterTest extends TestWithCurator { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void setupZK() { + configCurator.createNode("/sessions"); + configCurator.createNode("/sessions/1"); + configCurator.createNode("/sessions/2"); + } + + @Test + public void requireThatCounterIsInitializedFromNumberOfSessions() { + InitializedCounter counter = new InitializedCounter(curator, "/counter", "/sessions"); + assertThat(counter.counter.get(), is(2l)); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java new file mode 100644 index 00000000000..6205ac09c4c --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFileTest.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.google.common.io.Files; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.ApplicationFileTest; +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.mock.MockCurator; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertTrue; + +/** + * @author lulf + * @since 5.1 + */ +public class ZKApplicationFileTest extends ApplicationFileTest { + + private void feed(ConfigCurator zk, File dirToFeed) throws IOException { + assertTrue(dirToFeed.isDirectory()); + String appPath = "/0"; + zk.feedZooKeeper(dirToFeed, appPath + ConfigCurator.USERAPP_ZK_SUBPATH, null, true); + zk.putData(appPath, ZKApplicationPackage.fileRegistryNode, "dummyfiles"); + } + + @Override + public ApplicationFile getApplicationFile(Path path) throws IOException{ + ConfigCurator configCurator = ConfigCurator.create(new MockCurator()); + File tmp = Files.createTempDir(); + writeAppTo(tmp); + feed(configCurator, tmp); + return new ZKApplicationFile(path, new ZKLiveApp(configCurator, Path.fromString("/0"))); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java new file mode 100644 index 00000000000..217e2a04f9b --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackageTest.java @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.util.Collections; +import java.util.regex.Pattern; + +import com.yahoo.config.application.api.DeploymentSpec; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.Version; +import com.yahoo.path.Path; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.TestWithCurator; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.yahoo.io.IOUtils; + +public class ZKApplicationPackageTest extends TestWithCurator { + + private static final String APP = "src/test/apps/zkapp"; + private static final ProvisionInfo provisionInfo = ProvisionInfo.withHosts( + Collections.singleton(new HostSpec("foo.yahoo.com", Collections.emptyList()))); + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + @Test + public void testBasicZKFeed() throws IOException { + feed(configCurator, new File(APP)); + ZKApplicationPackage zkApp = new ZKApplicationPackage(configCurator, Path.fromString("/0")); + assertTrue(Pattern.compile(".*<slobroks>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getServices())).matches()); + assertTrue(Pattern.compile(".*<alias>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getHosts())).matches()); + assertTrue(Pattern.compile(".*<slobroks>.*",Pattern.MULTILINE+Pattern.DOTALL).matcher(IOUtils.readAll(zkApp.getFile(Path.fromString("services.xml")).createReader())).matches()); + DeployState deployState = new DeployState.Builder().applicationPackage(zkApp).build(); + assertEquals(deployState.getSearchDefinitions().size(), 5); + assertEquals(zkApp.searchDefinitionContents().size(), 5); + assertEquals(IOUtils.readAll(zkApp.getRankingExpression("foo.expression")), "foo()+1\n"); + assertEquals(zkApp.getFiles(Path.fromString(""), "xml").size(), 3); + assertEquals(zkApp.getFileReference(Path.fromString("components/file.txt")).getAbsolutePath(), "/home/vespa/test/file.txt"); + try (Reader foo = zkApp.getFile(Path.fromString("files/foo.json")).createReader()) { + assertEquals(IOUtils.readAll(foo), "foo : foo\n"); + } + try (Reader bar = zkApp.getFile(Path.fromString("files/sub/bar.json")).createReader()) { + assertEquals(IOUtils.readAll(bar), "bar : bar\n"); + } + assertTrue(zkApp.getFile(Path.createRoot()).exists()); + assertTrue(zkApp.getFile(Path.createRoot()).isDirectory()); + Version goodVersion = Version.fromIntValues(3, 0, 0); + assertTrue(zkApp.getFileRegistryMap().containsKey(goodVersion)); + assertFalse(zkApp.getFileRegistryMap().containsKey(Version.fromIntValues(0, 0, 0))); + assertThat(zkApp.getFileRegistryMap().get(goodVersion).fileSourceHost(), is("dummyfiles")); + assertTrue(zkApp.getProvisionInfoMap().containsKey(goodVersion)); + ProvisionInfo readInfo = zkApp.getProvisionInfoMap().get(goodVersion); + assertThat(Utf8.toString(readInfo.toJson()), is(Utf8.toString(provisionInfo.toJson()))); + assertTrue(zkApp.getDeployment().isPresent()); + assertThat(DeploymentSpec.fromXml(zkApp.getDeployment().get()).globalServiceId().get(), is("mydisc")); + } + + private void feed(ConfigCurator zk, File dirToFeed) throws IOException { + assertTrue(dirToFeed.isDirectory()); + zk.feedZooKeeper(dirToFeed, "/0" + ConfigCurator.USERAPP_ZK_SUBPATH, null, true); + String metaData = "{\"deploy\":{\"user\":\"foo\",\"from\":\"bar\",\"timestamp\":1},\"application\":{\"name\":\"foo\",\"checksum\":\"abc\",\"generation\":4,\"previousActiveGeneration\":3}}"; + zk.putData("/0", ConfigCurator.META_ZK_PATH, metaData); + zk.putData("/0/" + ZKApplicationPackage.fileRegistryNode + "/3.0.0", "dummyfiles"); + zk.putData("/0/" + ZKApplicationPackage.allocatedHostsNode + "/3.0.0", provisionInfo.toJson()); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java b/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java new file mode 100644 index 00000000000..cbaa8f36805 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/serviceview/ServiceModelTest.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import com.yahoo.vespa.defaults.Defaults; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.yahoo.vespa.serviceview.bindings.ApplicationView; +import com.yahoo.vespa.serviceview.bindings.HostService; +import com.yahoo.vespa.serviceview.bindings.ModelResponse; +import com.yahoo.vespa.serviceview.bindings.ServicePort; +import com.yahoo.vespa.serviceview.bindings.ServiceView; + +/** + * Functional tests for the programmatic view of cloud.config.model. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ServiceModelTest { + + ServiceModel model; + + @Before + public void setUp() throws Exception { + ModelResponse model = syntheticModelResponse(); + this.model = new ServiceModel(model); + } + + static ModelResponse syntheticModelResponse() { + ModelResponse model = new ModelResponse(); + HostService h = new HostService(); + h.name = "vespa.yahoo.com"; + com.yahoo.vespa.serviceview.bindings.Service service0 = new com.yahoo.vespa.serviceview.bindings.Service(); + { + service0.clustername = "examplecluster"; + service0.clustertype = "somethingservers"; + service0.index = 1L; + service0.type = "something"; + service0.name = "examplename"; + service0.configid = "blblb/lbl.0"; + ServicePort port = new ServicePort(); + port.number = Defaults.getDefaults().vespaWebServicePort(); + port.tags = "state http"; + service0.ports = Arrays.asList(new ServicePort[] { port }); + } + com.yahoo.vespa.serviceview.bindings.Service service1 = new com.yahoo.vespa.serviceview.bindings.Service(); + { + service1.clustername = "examplecluster"; + service1.clustertype = "somethingservers"; + service1.index = 2L; + service1.type = "container-clustercontroller"; + service1.name = "clustercontroller"; + service1.configid = "clustercontroller/lbl.0"; + ServicePort port = new ServicePort(); + port.number = 4090; + port.tags = "state http"; + service1.ports = Arrays.asList(new ServicePort[] { port }); + } + com.yahoo.vespa.serviceview.bindings.Service service2 = new com.yahoo.vespa.serviceview.bindings.Service(); + { + service2.clustername = "tralala"; + service2.clustertype = "admin"; + service2.index = 3L; + service2.type = "configserver"; + service2.name = "configservername"; + service2.configid = "clustercontroller/lbl.0"; + ServicePort port = new ServicePort(); + port.number = 5000; + port.tags = "state http"; + service2.ports = Arrays.asList(new ServicePort[] { port }); + } + h.services = Arrays.asList(new com.yahoo.vespa.serviceview.bindings.Service[] { service0, service1, service2 }); + model.hosts = Arrays.asList(new HostService[] { h }); + return model; + } + + @After + public void tearDown() throws Exception { + model = null; + } + + @Test + public final void test() { + final String uriBase = "http://configserver:5000/"; + ApplicationView x = model.showAllClusters(uriBase, "/tenant/default/application/default"); + assertEquals(2, x.clusters.size()); + String urlTracking = null; + for (com.yahoo.vespa.serviceview.bindings.ClusterView c : x.clusters) { + for (ServiceView s : c.services) { + if ("examplename".equals(s.serviceName)) { + assertEquals("something", s.serviceType); + urlTracking = s.url; + break; + } + } + } + assertNotNull(urlTracking); + final String serviceIdentifier = urlTracking.substring(urlTracking.indexOf("something"), + urlTracking.length() - "/state/v1/".length()); + Service y = model.getService(serviceIdentifier); + assertEquals("examplename", y.name); + } + +} diff --git a/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java b/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java new file mode 100644 index 00000000000..0f577030984 --- /dev/null +++ b/configserver/src/test/java/com/yahoo/vespa/serviceview/StateResourceTest.java @@ -0,0 +1,96 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import static org.junit.Assert.*; + +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.UriInfo; + +import com.yahoo.vespa.defaults.Defaults; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.vespa.serviceview.bindings.ApplicationView; +import com.yahoo.vespa.serviceview.bindings.HealthClient; +import com.yahoo.vespa.serviceview.bindings.ModelResponse; + +/** + * Functional test for {@link StateResource}. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class StateResourceTest { + + private static final String EXTERNAL_BASE_URI = "http://someserver:8080/serviceview/"; + + private static class TestResource extends StateResource { + private static final String BASE_URI = "http://vespa.yahoo.com:8080/state/v1"; + + TestResource(@Component ConfigServerLocation configServer, @Context UriInfo ui) { + super(configServer, ui); + } + + @Override + protected ModelResponse getModelConfig(String tenant, String application, String environment, String region, String instance) { + return ServiceModelTest.syntheticModelResponse(); + } + + @Override + protected HealthClient getHealthClient(String apiParams, Service s, int requestedPort, Client client) { + HealthClient healthClient = Mockito.mock(HealthClient.class); + HashMap<Object, Object> dummyHealthData = new HashMap<>(); + HashMap<String, String> dummyLink = new HashMap<>(); + dummyLink.put("url", BASE_URI); + dummyHealthData.put("resources", Arrays.asList(dummyLink)); + Mockito.when(healthClient.getHealthInfo()).thenReturn(dummyHealthData); + return healthClient; + } + } + + StateResource testResource; + ServiceModel correspondingModel; + + @Before + public void setUp() throws Exception { + UriInfo base = Mockito.mock(UriInfo.class); + Mockito.when(base.getBaseUri()).thenReturn(new URI(EXTERNAL_BASE_URI)); + ConfigServerLocation dummyLocation = new ConfigServerLocation(new ConfigserverConfig(new ConfigserverConfig.Builder())); + testResource = new TestResource(dummyLocation, base); + correspondingModel = new ServiceModel(ServiceModelTest.syntheticModelResponse()); + } + + @After + public void tearDown() throws Exception { + testResource = null; + correspondingModel = null; + } + + @SuppressWarnings("rawtypes") + @Test + public final void test() { + Service s = correspondingModel.resolve("vespa.yahoo.com", 8080, null); + String api = "/state/v1"; + HashMap boom = testResource.singleService("default", "default", "default", "default", "default", s.getIdentifier(8080), api); + assertEquals(EXTERNAL_BASE_URI + "v1/tenant/default/application/default/environment/default/region/default/instance/default/service/" + s.getIdentifier(8080) + api, + ((Map) ((List) boom.get("resources")).get(0)).get("url")); + } + + @Test + public final void testLinkEquality() { + ApplicationView explicitParameters = testResource.getUserInfo("default", "default", "default", "default", "default"); + ApplicationView implicitParameters = testResource.getDefaultUserInfo(); + assertEquals(explicitParameters.clusters.get(0).services.get(0).url, implicitParameters.clusters.get(0).services.get(0).url); + } + +} |