diff options
author | Erlend <erlendniko@hotmail.com> | 2022-08-11 13:24:41 +0200 |
---|---|---|
committer | Erlend <erlendniko@hotmail.com> | 2022-08-11 13:24:41 +0200 |
commit | 5d94611148044c32ef24408ee6935ab2ce664e82 (patch) | |
tree | e18f34065ca51ed2ea3c5e6adf90118e967ddb01 | |
parent | a3db1c61f4da93b024eba736d22dffef66d66afe (diff) | |
parent | acfbe53b1c03d7dcdc2d6947970de8d351ae8059 (diff) |
Merge remote-tracking branch 'upstream/master'
11 files changed, 180 insertions, 164 deletions
diff --git a/client/README.md b/client/README.md index a459ea65b1d..8403e02a485 100644 --- a/client/README.md +++ b/client/README.md @@ -1,19 +1,45 @@ <!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +![Vespa logo](https://vespa.ai/assets/vespa-logo-color.png) + # Vespa clients +This part of the Vespa repository got Vespa client implementations for operations like +* deploy +* read/write +* query + +<!-- ToDo: illustration --> + + ## Vespa CLI The Vespa command-line tool, see the [README](go/README.md). +Use the Vespa CLI to deploy, feed and query a Vespa application, +for local, self-hosted or [Vespa Cloud](https://cloud.vespa.ai/) instances. + + + +## pyvespa +[pyvespa](https://pyvespa.readthedocs.io/) provides a python API to Vespa - +use it to create, modify, deploy and interact with running Vespa instances. +The main pyvespa goal is to allow for faster prototyping +and to facilitate Machine Learning experiments for Vespa applications. + ## Vespa FE (fixme: better name and description here) -This is a work-in-progress javascript app for various use cases. +This is a [work-in-progress javascript app](js/app) for querying a Vespa application. + -## vespa_query_dsl +---- + +## Misc + +<!-- ToDo: move this / demote this somehow --> +### vespa_query_dsl This lib is used for composing Vespa [YQL queries](https://docs.vespa.ai/en/reference/query-language-reference.html). - For usage, refer to the [QTest.java](src/test/java/ai/vespa/client/dsl/QTest.java) unit test. ToDos: diff --git a/client/js/app/README.md b/client/js/app/README.md index 99271629edf..ae6a8d1cc25 100644 --- a/client/js/app/README.md +++ b/client/js/app/README.md @@ -1,48 +1,57 @@ <!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +![Vespa logo](https://vespa.ai/assets/vespa-logo-color.png) + # Vespa client This app is work in progress. +It currently contains the **Query Builder** and the **Trace Visualizer**. + -This client currently contains the **Query Builder** and the **Trace Visualizer**. -# Query Builder +## Query Builder +The Query Builder is a tool for creating Vespa queries to send to a local backend. +The tool provides all of the options for query parameters from dropdowns. +The input fields provide hints to what is the expected type of value. -The Query Builder is a tool for creating Vespa queries to send to a local backend. -The tool provides all of the options for query parameters from dropdowns. The input fields -provide hints to what is the expected type of value. -# Trace Visualizer -The Trace Visualizer is a tool for converting and visualizing traces from Vespa in a flame graph. -To use the visualizer, a [Jaeger](https://www.jaegertracing.io/) instance must be run locally with Docker. +## Trace Visualizer +The Trace Visualizer is a tool for converting and visualizing traces from Vespa in a flame graph. +To use the visualizer, a [Jaeger](https://www.jaegertracing.io/) instance must be run locally with Docker: docker run -d --rm \ - -p 16685:16685 \ - -p 16686:16686 \ - -p 16687:16687 \ - -e SPAN_STORAGE_TYPE=memory \ - jaegertracing/jaeger-query:latest + -p 16685:16685 \ + -p 16686:16686 \ + -p 16687:16687 \ + -e SPAN_STORAGE_TYPE=memory \ + jaegertracing/jaeger-query:latest The Jaeger UI can then be reached at **localhost:16686/search** -To use the visualizer you paste the Vespa trace into the text box and press the button to convert the trace -to a format supported by Jaeger and download it. -Only Vespa traces using _trace.timestampa=true_ **and** _traceLevel_ between 3 and 5 (inclusive) will work correctly. +To use the visualizer, +paste the Vespa trace into the text box +and press the button to convert the trace to a format supported by Jaeger and download it. +Only traces using _trace.timestamps=true_ **and** _traceLevel_ between 3 and 5 (inclusive) will work correctly - +see [query tracing](https://docs.vespa.ai/en/query-api.html#query-tracing): ![Trace Converter](img/TraceConverter.png) -After downloading the converted trace is can be used with the Jaeger UI. -Press the _JSON File_ button as shown in the image, and drag and drop the trace you just downloaded. +After downloading the converted trace, it can be used with the Jaeger UI. +Press the _JSON File_ button as shown in the image, and drag and drop the trace you just downloaded: -![Jager Image](img/JaegerExample.png) +![Jaeger Image](img/JaegerExample.png) -You can then click on the newly added trace and see it displayed as a flame graph. +Then click on the newly added trace and see it displayed as a flame graph: ![Example Image](img/result.png) -# Client install and start + + +## Client install and start nvm install --lts node # in case your current node.js is too old yarn install yarn dev # then open link, like http://127.0.0.1:3000/ + +<!-- ToDo: publish a Docker image with all the clients ... --> diff --git a/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java b/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java index ae7ffd248d6..fb468ee4d5b 100644 --- a/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java +++ b/clustercontroller-core/src/test/java/com/yahoo/vespa/clustercontroller/core/MasterElectionTest.java @@ -12,6 +12,7 @@ import com.yahoo.vdslib.state.NodeState; import com.yahoo.vdslib.state.NodeType; import com.yahoo.vdslib.state.State; import com.yahoo.vespa.clustercontroller.core.status.StatusHandler; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -290,6 +291,7 @@ public class MasterElectionTest extends FleetControllerTest { } @Test + @Disabled("Unstable, disable test, as functionality is not deemed critical") void testMasterZooKeeperCooldown() throws Exception { startingTest("MasterElectionTest::testMasterZooKeeperCooldown"); FleetControllerOptions options = defaultOptions("mycluster"); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java index 1798409d3d0..2348970ed1a 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/DefaultMetrics.java @@ -55,11 +55,20 @@ public class DefaultMetrics { Set<Metric> metrics = new LinkedHashSet<>(); metrics.add(new Metric("queries.rate")); - metrics.add(new Metric("query_latency.average")); + metrics.add(new Metric("query_latency.sum")); + metrics.add(new Metric("query_latency.count")); + metrics.add(new Metric("query_latency.max")); + metrics.add(new Metric("query_latency.average")); // TODO: Remove with Vespa 9 metrics.add(new Metric("query_latency.95percentile")); metrics.add(new Metric("query_latency.99percentile")); - metrics.add(new Metric("hits_per_query.average")); - metrics.add(new Metric("totalhits_per_query.average")); + metrics.add(new Metric("hits_per_query.sum")); + metrics.add(new Metric("hits_per_query.count")); + metrics.add(new Metric("hits_per_query.max")); + metrics.add(new Metric("hits_per_query.average")); // TODO: Remove with Vespa 9 + metrics.add(new Metric("totalhits_per_query.sum")); + metrics.add(new Metric("totalhits_per_query.count")); + metrics.add(new Metric("totalhits_per_query.max")); + metrics.add(new Metric("totalhits_per_query.average")); // TODO: Remove with Vespa 9 metrics.add(new Metric("degraded_queries.rate")); metrics.add(new Metric("failed_queries.rate")); metrics.add(new Metric("serverActiveThreads.average")); @@ -71,8 +80,14 @@ public class DefaultMetrics { Set<Metric> metrics = new LinkedHashSet<>(); metrics.add(new Metric("content.proton.search_protocol.docsum.requested_documents.rate")); - metrics.add(new Metric("content.proton.search_protocol.docsum.latency.average")); - metrics.add(new Metric("content.proton.search_protocol.query.latency.average")); + metrics.add(new Metric("content.proton.search_protocol.docsum.latency.sum")); + metrics.add(new Metric("content.proton.search_protocol.docsum.latency.count")); + metrics.add(new Metric("content.proton.search_protocol.docsum.latency.max")); + metrics.add(new Metric("content.proton.search_protocol.docsum.latency.average")); // TODO: Remove with Vespa 9 + metrics.add(new Metric("content.proton.search_protocol.query.latency.sum")); + metrics.add(new Metric("content.proton.search_protocol.query.latency.count")); + metrics.add(new Metric("content.proton.search_protocol.query.latency.max")); + metrics.add(new Metric("content.proton.search_protocol.query.latency.average")); // TODO: Remove with Vespa 9 metrics.add(new Metric("content.proton.documentdb.documents.total.last")); metrics.add(new Metric("content.proton.documentdb.documents.ready.last")); @@ -85,9 +100,18 @@ public class DefaultMetrics { metrics.add(new Metric("content.proton.documentdb.matching.docs_matched.rate")); metrics.add(new Metric("content.proton.documentdb.matching.docs_reranked.rate")); - metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.average")); - metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.average")); - metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.average")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.sum")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.count")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.max")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_setup_time.average")); // TODO: Remove with Vespa 9 + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.sum")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.count")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.max")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.query_latency.average")); // TODO: Remove with Vespa 9 + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.sum")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.count")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.max")); + metrics.add(new Metric("content.proton.documentdb.matching.rank_profile.rerank_time.average")); // TODO: Remove with Vespa 9 metrics.add(new Metric("content.proton.transactionlog.disk_usage.last")); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 423d82687a3..f6174e1740f 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -551,7 +551,10 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { cluster.setSearch(buildSearch(deployState, cluster, searchElement)); addSearchHandler(cluster, searchElement); - addGUIHandler(cluster); + + // Set up GUI handler only on self hosted + if (!deployState.isHosted()) + addGUIHandler(cluster); validateAndAddConfiguredComponents(deployState, cluster, searchElement, "renderer", ContainerModelBuilder::validateRendererElement); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index 38d7b6d3a2b..ef3474e0c1e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -656,10 +656,13 @@ public class InternalStepRunner implements StepRunner { controller.jobController().updateTestReport(id); return Optional.of(testFailure); case INCONCLUSIVE: - long sleepMinutes = Math.max(15, Math.min(120, Duration.between(deployment.get().at(), controller.clock().instant()).toMinutes() / 20)); - logger.log("Tests were inconclusive, and will run again in " + sleepMinutes + " minutes."); controller.jobController().updateTestReport(id); - controller.jobController().locked(id, run -> run.sleepingUntil(controller.clock().instant().plusSeconds(60 * sleepMinutes))); + controller.jobController().locked(id, run -> { + Instant nextAttemptAt = run.start(); + while ( ! nextAttemptAt.isAfter(controller.clock().instant())) nextAttemptAt = nextAttemptAt.plusSeconds(1800); + logger.log("Tests were inconclusive, and will run again at " + nextAttemptAt + "."); + return run.sleepingUntil(nextAttemptAt); + }); return Optional.of(reset); case ERROR: logger.log(INFO, "Tester failed running its tests!"); @@ -806,6 +809,7 @@ public class InternalStepRunner implements StepRunner { Consumer<String> updater = msg -> controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.error, msg); switch (run.status()) { case aborted: return; // wait and see how the next run goes. + case noTests: case running: case success: controller.notificationsDb().removeNotification(source, Notification.Type.deployment); @@ -822,10 +826,6 @@ public class InternalStepRunner implements StepRunner { case testFailure: updater.accept("one or more verification tests against the deployment failed. Please review test output in the deployment job log."); return; - case noTests: - controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.warning, - "no tests were found for this job type. Please review test output in the deployment job log."); - return; case error: case endpointCertificateTimeout: break; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java index bf488198126..493c0945ecc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java @@ -358,7 +358,8 @@ public class InternalStepRunnerTest { // Test sleeps for a while. tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); - tester.clock().advance(Duration.ofSeconds(899)); + Instant nextAttemptAt = tester.clock().instant().plusSeconds(1800); + tester.clock().advance(Duration.ofSeconds(1799)); tester.runner().run(); assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester)); @@ -380,8 +381,8 @@ public class InternalStepRunnerTest { assertTestLogEntries(id, Step.endTests, new LogEntry(lastId1 + 1, Instant.ofEpochMilli(123), info, "Not enough data!"), - new LogEntry(lastId1 + 2, instant1, info, "Tests were inconclusive, and will run again in 15 minutes."), - new LogEntry(lastId1 + 15, instant1, info, "### Run will reset, and start over at " + instant1.plusSeconds(900).truncatedTo(SECONDS)), + new LogEntry(lastId1 + 2, instant1, info, "Tests were inconclusive, and will run again at " + nextAttemptAt + "."), + new LogEntry(lastId1 + 15, instant1, info, "### Run will reset, and start over at " + nextAttemptAt), new LogEntry(lastId1 + 16, instant1, info, ""), new LogEntry(lastId2 + 1, tester.clock().instant(), info, "Tests completed successfully.")); diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java index ef4b402d33b..756c3f55ab3 100644 --- a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java +++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/TestRunnerHandler.java @@ -150,61 +150,10 @@ public class TestRunnerHandler extends ThreadedHttpRequestHandler { json.writeFieldName("report"); render(json, (Node) report.root()); - // TODO jonmv: remove - json.writeObjectFieldStart("summary"); - - renderSummary(json, report); - - json.writeArrayFieldStart("failures"); - renderFailures(json, report.root()); - json.writeEndArray(); - - json.writeEndObject(); - - // TODO jonmv: remove - json.writeArrayFieldStart("output"); - renderOutput(json, report.root()); - json.writeEndArray(); - json.writeEndObject(); json.close(); } - private static void renderSummary(JsonGenerator json, TestReport report) throws IOException { - Map<TestReport.Status, Long> tally = report.root().tally(); - json.writeNumberField("success", tally.getOrDefault(TestReport.Status.successful, 0L)); - json.writeNumberField("failed", tally.getOrDefault(TestReport.Status.failed, 0L) + tally.getOrDefault(TestReport.Status.error, 0L)); - json.writeNumberField("ignored", tally.getOrDefault(TestReport.Status.skipped, 0L)); - json.writeNumberField("aborted", tally.getOrDefault(TestReport.Status.aborted, 0L)); - json.writeNumberField("inconclusive", tally.getOrDefault(TestReport.Status.inconclusive, 0L)); - } - - private static void renderFailures(JsonGenerator json, Node node) throws IOException { - if (node instanceof FailureNode) { - json.writeStartObject(); - json.writeStringField("testName", node.parent.name()); - json.writeStringField("testError", ((FailureNode) node).thrown().getMessage()); - json.writeStringField("exception", ExceptionUtils.getStackTraceAsString(((FailureNode) node).thrown())); - json.writeEndObject(); - } - else { - for (Node child : node.children()) - renderFailures(json, child); - } - } - - private static void renderOutput(JsonGenerator json, Node node) throws IOException { - if (node instanceof OutputNode) { - for (LogRecord record : ((OutputNode) node).log()) - if (record.getMessage() != null) - json.writeString(formatter.format(record.getInstant().atOffset(ZoneOffset.UTC)) + " " + record.getMessage()); - } - else { - for (Node child : node.children()) - renderOutput(json, child); - } - } - private static void render(JsonGenerator json, Node node) throws IOException { json.writeStartObject(); if (node instanceof NamedNode) render(json, (NamedNode) node); diff --git a/vespa-osgi-testrunner/src/test/resources/report.json b/vespa-osgi-testrunner/src/test/resources/report.json index 443694e2e0c..66ae6dd398c 100644 --- a/vespa-osgi-testrunner/src/test/resources/report.json +++ b/vespa-osgi-testrunner/src/test/resources/report.json @@ -482,74 +482,5 @@ ] } ] - }, - "summary": { - "success": 3, - "failed": 5, - "ignored": 4, - "aborted": 1, - "inconclusive": 1, - "failures": [ - { - "testName": "error()", - "testError": null, - "exception": "java.lang.NoClassDefFoundError\n\tat com.yahoo.vespa.test.samples.SampleTest.error(SampleTest.java:87)\n" - }, - { - "testName": "failing()", - "testError": "baz ==> expected: <foo> but was: <bar>", - "exception": "org.opentest4j.AssertionFailedError: baz ==> expected: <foo> but was: <bar>\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)\n\tat org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)\n\tat org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)\n\tat org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1152)\n\tat com.yahoo.vespa.test.samples.SampleTest.failing(SampleTest.java:81)\n" - }, - { - "testName": "inconclusive(TestReporter)", - "testError": "the cat is both dead _and_ alive", - "exception": "ai.vespa.hosted.cd.InconclusiveTestException: the cat is both dead _and_ alive\n\tat com.yahoo.vespa.test.samples.SampleTest.inconclusive(SampleTest.java:93)\n" - }, - { - "testName": "third", - "testError": "no charm", - "exception": "org.opentest4j.AssertionFailedError: no charm\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)\n\tat org.junit.jupiter.api.Assertions.fail(Assertions.java:134)\n\tat com.yahoo.vespa.test.samples.SampleTest$Inner.lambda$others$1(SampleTest.java:105)\n" - }, - { - "testName": "test()", - "testError": "", - "exception": "org.opentest4j.AssertionFailedError\n\tat org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:35)\n\tat org.junit.jupiter.api.Assertions.fail(Assertions.java:115)\n\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.test(FailingTestAndBothAftersTest.java:19)\n\tSuppressed: java.lang.RuntimeException\n\t\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.moreFail(FailingTestAndBothAftersTest.java:16)\n" - }, - { - "testName": "FailingTestAndBothAftersTest", - "testError": null, - "exception": "java.lang.RuntimeException\n\tat com.yahoo.vespa.test.samples.FailingTestAndBothAftersTest.fail(FailingTestAndBothAftersTest.java:13)\n" - }, - { - "testName": "WrongBeforeAllTest", - "testError": "@BeforeAll method 'void com.yahoo.vespa.test.samples.WrongBeforeAllTest.wrong()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).", - "exception": "org.junit.platform.commons.JUnitException: @BeforeAll method 'void com.yahoo.vespa.test.samples.WrongBeforeAllTest.wrong()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).\n" - }, - { - "testName": "test()", - "testError": null, - "exception": "java.lang.NullPointerException\n\tat com.yahoo.vespa.test.samples.FailingExtensionTest$FailingExtension.<init>(FailingExtensionTest.java:19)\n" - }, - { - "testName": "Production test", - "testError": "School's out all summer!", - "exception": "java.lang.ClassNotFoundException: School's out all summer!\n" - } - ] - }, - "output": [ - "00:00:00.000 spam", - "00:00:00.000 spam", - "00:00:00.000 spam", - "00:00:00.000 I have a bad feeling about this", - "00:00:00.000 spam", - "00:00:00.000 I'm here with Erwin today; Erwin, what can you tell us about your cat?", - "00:00:00.000 spam", - "00:00:00.000 <body />", - "00:00:00.000 Very informative: \"\\n\": \n", - "00:00:00.000 Oh no", - "00:00:00.000 spam", - "00:00:00.000 Catch me if you can!", - "00:00:00.000 spam" - ] + } } diff --git a/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java b/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java new file mode 100644 index 00000000000..0ae81e2315d --- /dev/null +++ b/vespajlib/src/main/java/ai/vespa/validation/PathValidator.java @@ -0,0 +1,36 @@ +package ai.vespa.validation; + +import java.nio.file.Path; + +/** + * Path validations + * + * @author mortent + */ +public class PathValidator { + + /** + * Validate that file is a child of basedir + * @param root Root directory to use for validation + * @param path Path to validate + * @throws IllegalArgumentException if path is not a child of root + */ + public static void validateChildOf(Path root, Path path) { + if (!path.normalize().startsWith(root)) { + throw new IllegalArgumentException("Invalid path %s".formatted(path)); + } + } + + /** + * Resolves a path under a root path + * @param root root poth + * @param path child to resolve + * @return The resolved path + * @throws IllegalArgumentException If the provided child path does not resolve as child of root + */ + public static Path resolveChildOf(Path root, String path) { + Path resolved = root.resolve(path); + validateChildOf(root, resolved); + return resolved; + } +} diff --git a/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java b/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java new file mode 100644 index 00000000000..a2c1bd6bd0c --- /dev/null +++ b/vespajlib/src/test/java/ai/vespa/validation/PathValidatorTest.java @@ -0,0 +1,35 @@ +package ai.vespa.validation; + +import org.junit.Test; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PathValidatorTest { + + @Test + public void testPathValidation() { + Path root = Path.of("/foo/"); + + assertOk(Path.of("/foo/bar"), root); + assertOk(Path.of("/foo/foo2/bar"), root); + assertOk(Path.of("/foo/foo2/../bar"), root); + assertOk(Path.of("/foo/../foo/bar"), root); + assertOk(Path.of("/bar/../foo/../foo/bar"), root); + + assertInvalid(Path.of("/foo/../bar"), root); + assertInvalid(Path.of("/foo/bar/../../bar"), root); + } + + private void assertOk(Path path, Path root) { + PathValidator.validateChildOf(root, path); + } + + private void assertInvalid(Path path, Path root) { + IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, + () -> PathValidator.validateChildOf(root, path)); + assertEquals("Invalid path %s".formatted(path), illegalArgumentException.getMessage()); + } +} |