diff options
author | Martin Polden <mpolden@mpolden.no> | 2019-07-16 10:30:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-07-16 10:30:03 +0200 |
commit | ed66c3239ed498be526b1e7543d2d768beca3910 (patch) | |
tree | fe3e03ce911010653b974ca5316b96777b7aca50 | |
parent | 5d95bfb487229d05d67554a6890e056db0db001f (diff) | |
parent | 67e2766163bf1dba13c8cce56e2bbb3af7198ea3 (diff) |
Merge pull request #10041 from vespa-engine/mpolden/move-flags-api
Audit log /flags/v1 on controller
21 files changed, 331 insertions, 91 deletions
diff --git a/configserver-flags/pom.xml b/configserver-flags/pom.xml index 8c96512c4c0..11ef9b6c950 100644 --- a/configserver-flags/pom.xml +++ b/configserver-flags/pom.xml @@ -20,6 +20,12 @@ <!-- provided --> <dependency> <groupId>com.yahoo.vespa</groupId> + <artifactId>container-dev</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> <artifactId>zkfacade</artifactId> <version>${project.version}</version> <scope>provided</scope> diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/DefinedFlag.java index 92397fc84a7..c706a2b1e51 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlag.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/DefinedFlag.java @@ -1,12 +1,11 @@ // Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.Response; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; import com.yahoo.vespa.flags.FlagDefinition; import com.yahoo.vespa.flags.json.DimensionHelper; @@ -42,6 +41,7 @@ public class DefinedFlag extends HttpResponse { @Override public String getContentType() { - return HttpConfigResponse.JSON_CONTENT_TYPE; + return "application/json"; } + } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/DefinedFlags.java index 9604c51ee4b..26d590593c0 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/DefinedFlags.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/DefinedFlags.java @@ -1,11 +1,10 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.Response; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; import com.yahoo.vespa.flags.FlagDefinition; import java.io.IOException; @@ -18,8 +17,7 @@ import java.util.List; */ public class DefinedFlags extends HttpResponse { private static ObjectMapper mapper = new ObjectMapper(); - private static final Comparator<FlagDefinition> sortByFlagId = - (left, right) -> left.getUnboundFlag().id().compareTo(right.getUnboundFlag().id()); + private static final Comparator<FlagDefinition> sortByFlagId = Comparator.comparing(flagDefinition -> flagDefinition.getUnboundFlag().id()); private final List<FlagDefinition> flags; @@ -40,6 +38,6 @@ public class DefinedFlags extends HttpResponse { @Override public String getContentType() { - return HttpConfigResponse.JSON_CONTENT_TYPE; + return "application/json"; } } diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/ErrorResponse.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/ErrorResponse.java new file mode 100644 index 00000000000..b9e5c75fe22 --- /dev/null +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/ErrorResponse.java @@ -0,0 +1,66 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.configserver.flags.http; + +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; + +import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; +import static com.yahoo.jdisc.Response.Status.FORBIDDEN; +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED; + +/** + * A HTTP JSON response containing an error code and a message + * + * @author bratseth + */ +public class ErrorResponse extends SlimeJsonResponse { + + public enum errorCodes { + NOT_FOUND, + BAD_REQUEST, + FORBIDDEN, + METHOD_NOT_ALLOWED, + INTERNAL_SERVER_ERROR, + UNAUTHORIZED + } + + public ErrorResponse(int statusCode, String errorType, String message) { + super(statusCode, asSlimeMessage(errorType, message)); + } + + private static Slime asSlimeMessage(String errorType, String message) { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + root.setString("error-code", errorType); + root.setString("message", message); + return slime; + } + + public static ErrorResponse notFoundError(String message) { + return new ErrorResponse(NOT_FOUND, errorCodes.NOT_FOUND.name(), message); + } + + public static ErrorResponse internalServerError(String message) { + return new ErrorResponse(INTERNAL_SERVER_ERROR, errorCodes.INTERNAL_SERVER_ERROR.name(), message); + } + + public static ErrorResponse badRequest(String message) { + return new ErrorResponse(BAD_REQUEST, errorCodes.BAD_REQUEST.name(), message); + } + + public static ErrorResponse forbidden(String message) { + return new ErrorResponse(FORBIDDEN, errorCodes.FORBIDDEN.name(), message); + } + + public static ErrorResponse unauthorized(String message) { + return new ErrorResponse(UNAUTHORIZED, errorCodes.UNAUTHORIZED.name(), message); + } + + public static ErrorResponse methodNotAllowed(String message) { + return new ErrorResponse(METHOD_NOT_ALLOWED, errorCodes.METHOD_NOT_ALLOWED.name(), message); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagDataListResponse.java index b33fc7c2b04..efc78cb7930 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataListResponse.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagDataListResponse.java @@ -1,12 +1,11 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.Response; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; import com.yahoo.vespa.flags.FlagId; import com.yahoo.vespa.flags.json.FlagData; import com.yahoo.vespa.flags.json.wire.WireFlagDataList; @@ -54,6 +53,6 @@ public class FlagDataListResponse extends HttpResponse { @Override public String getContentType() { - return HttpConfigResponse.JSON_CONTENT_TYPE; + return "application/json"; } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagDataResponse.java index 054b218ff2d..8ff4085df8d 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagDataResponse.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagDataResponse.java @@ -1,9 +1,8 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.Response; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; import com.yahoo.vespa.flags.json.FlagData; import java.io.OutputStream; @@ -26,6 +25,6 @@ public class FlagDataResponse extends HttpResponse { @Override public String getContentType() { - return HttpConfigResponse.JSON_CONTENT_TYPE; + return "application/json"; } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagsHandler.java index 00f3d457d3d..40bb69111e0 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/FlagsHandler.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagsHandler.java @@ -1,14 +1,12 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.google.inject.Inject; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.log.LogLevel; import com.yahoo.restapi.Path; -import com.yahoo.vespa.config.server.http.HttpErrorResponse; -import com.yahoo.vespa.config.server.http.HttpHandler; -import com.yahoo.vespa.config.server.http.NotFoundException; import com.yahoo.vespa.configserver.flags.FlagsDb; import com.yahoo.vespa.flags.FlagDefinition; import com.yahoo.vespa.flags.FlagId; @@ -25,7 +23,8 @@ import java.util.Objects; * * @author hakonhall */ -public class FlagsHandler extends HttpHandler { +public class FlagsHandler extends LoggingRequestHandler { + private final FlagsDb flagsDb; @Inject @@ -35,28 +34,44 @@ public class FlagsHandler extends HttpHandler { } @Override - protected HttpResponse handleGET(HttpRequest request) { + public HttpResponse handle(HttpRequest request) { + try { + switch (request.getMethod()) { + case GET: return handleGET(request); + case DELETE: return handleDELETE(request); + case PUT: return handlePUT(request); + default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported"); + } + } + catch (IllegalArgumentException e) { + return ErrorResponse.badRequest(Exceptions.toMessageString(e)); + } + catch (RuntimeException e) { + log.log(LogLevel.WARNING, "Unexpected error handling '" + request.getUri() + "'", e); + return ErrorResponse.internalServerError(Exceptions.toMessageString(e)); + } + } + + private HttpResponse handleGET(HttpRequest request) { Path path = new Path(request.getUri()); if (path.matches("/flags/v1")) return new V1Response(flagsV1Uri(request), "data", "defined"); if (path.matches("/flags/v1/data")) return getFlagDataList(request); if (path.matches("/flags/v1/data/{flagId}")) return getFlagData(findFlagId(request, path)); if (path.matches("/flags/v1/defined")) return new DefinedFlags(Flags.getAllFlags()); if (path.matches("/flags/v1/defined/{flagId}")) return getDefinedFlag(findFlagId(request, path)); - throw new NotFoundException("Nothing at path '" + path + "'"); + return ErrorResponse.notFoundError("Nothing at path '" + path + "'"); } - @Override - protected HttpResponse handlePUT(HttpRequest request) { + private HttpResponse handlePUT(HttpRequest request) { Path path = new Path(request.getUri()); if (path.matches("/flags/v1/data/{flagId}")) return putFlagData(request, findFlagId(request, path)); - throw new NotFoundException("Nothing at path '" + path + "'"); + return ErrorResponse.notFoundError("Nothing at path '" + path + "'"); } - @Override - protected HttpResponse handleDELETE(HttpRequest request) { + private HttpResponse handleDELETE(HttpRequest request) { Path path = new Path(request.getUri()); if (path.matches("/flags/v1/data/{flagId}")) return deleteFlagData(findFlagId(request, path)); - throw new NotFoundException("Nothing at path '" + path + "'"); + return ErrorResponse.notFoundError("Nothing at path '" + path + "'"); } private String flagsV1Uri(HttpRequest request) { @@ -66,19 +81,24 @@ public class FlagsHandler extends HttpHandler { } private HttpResponse getDefinedFlag(FlagId flagId) { - FlagDefinition definition = Flags.getFlag(flagId) - .orElseThrow(() -> new NotFoundException("Flag " + flagId + " not defined")); - return new DefinedFlag(definition); + var definedFlag = Flags.getFlag(flagId).map(DefinedFlag::new); + if (definedFlag.isPresent()) { + return definedFlag.get(); + } + return ErrorResponse.notFoundError("Flag " + flagId + " not defined"); } private HttpResponse getFlagDataList(HttpRequest request) { return new FlagDataListResponse(flagsV1Uri(request), flagsDb.getAllFlags(), - Objects.equals(request.getProperty("recursive"), "true")); + Objects.equals(request.getProperty("recursive"), "true")); } private HttpResponse getFlagData(FlagId flagId) { - FlagData data = flagsDb.getValue(flagId).orElseThrow(() -> new NotFoundException("Flag " + flagId + " not set")); - return new FlagDataResponse(data); + var data = flagsDb.getValue(flagId).map(FlagDataResponse::new); + if (data.isPresent()) { + return data.get(); + } + return ErrorResponse.notFoundError("Flag " + flagId + " not set"); } private HttpResponse putFlagData(HttpRequest request, FlagId flagId) { @@ -86,7 +106,7 @@ public class FlagsHandler extends HttpHandler { try { data = FlagData.deserialize(request.getData()); } catch (UncheckedIOException e) { - return HttpErrorResponse.badRequest("Failed to deserialize request data: " + Exceptions.toMessageString(e)); + return ErrorResponse.badRequest("Failed to deserialize request data: " + Exceptions.toMessageString(e)); } if (!isForce(request)) { @@ -105,16 +125,14 @@ public class FlagsHandler extends HttpHandler { private FlagId findFlagId(HttpRequest request, Path path) { FlagId flagId = new FlagId(path.get("flagId")); - - if (!isForce(request)) { - Flags.getFlag(flagId).orElseThrow(() -> - new NotFoundException("There is no flag '" + flagId + "' (use ?force=true to override)")); + if (!isForce(request) && Flags.getFlag(flagId).isEmpty()) { + throw new IllegalArgumentException("There is no flag '" + flagId + "' (use ?force=true to override)"); } - return flagId; } private boolean isForce(HttpRequest request) { return Objects.equals(request.getProperty("force"), "true"); } + } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/OKResponse.java index 87c02ae56f1..f41940f692b 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/OKResponse.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/OKResponse.java @@ -1,9 +1,8 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.yahoo.container.jdisc.EmptyResponse; import com.yahoo.jdisc.Response; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; /** * @author hakonhall @@ -15,6 +14,6 @@ public class OKResponse extends EmptyResponse { @Override public String getContentType() { - return HttpConfigResponse.JSON_CONTENT_TYPE; + return "application/json"; } } diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/SlimeJsonResponse.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/SlimeJsonResponse.java new file mode 100644 index 00000000000..e5568514894 --- /dev/null +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/SlimeJsonResponse.java @@ -0,0 +1,38 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.configserver.flags.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A generic Json response using Slime for JSON encoding + * + * @author bratseth + */ +public class SlimeJsonResponse extends HttpResponse { + + private final Slime slime; + + public SlimeJsonResponse(Slime slime) { + super(200); + this.slime = slime; + } + + public SlimeJsonResponse(int statusCode, Slime slime) { + super(statusCode); + this.slime = slime; + } + + @Override + public void render(OutputStream stream) throws IOException { + new JsonFormat(true).encode(stream, slime); + } + + @Override + public String getContentType() { return "application/json"; } + +} diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/V1Response.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/V1Response.java new file mode 100644 index 00000000000..ac1e9514700 --- /dev/null +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/V1Response.java @@ -0,0 +1,46 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.configserver.flags.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.jdisc.Response; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * @author hakonhall + */ +public class V1Response extends HttpResponse { + + private final Slime slime; + + public V1Response(String flagsV1Uri, String... names) { + super(Response.Status.OK); + this.slime = generateBody(flagsV1Uri, List.of(names)); + } + + @Override + public void render(OutputStream stream) throws IOException { + new JsonFormat(true).encode(stream, slime); + } + + @Override + public String getContentType() { + return "application/json"; + } + + private static Slime generateBody(String flagsV1Uri, List<String> names) { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + names.forEach(name -> { + Cursor data = root.setObject(name); + data.setString("url", flagsV1Uri + "/" + name); + }); + return slime; + } + +} diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/package-info.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/package-info.java new file mode 100644 index 00000000000..87b63114b73 --- /dev/null +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author mpolden + */ +@ExportPackage +package com.yahoo.vespa.configserver.flags.http; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java index 97e66d95715..d6f078326a3 100644 --- a/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java +++ b/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/package-info.java @@ -3,5 +3,3 @@ package com.yahoo.vespa.configserver.flags; import com.yahoo.osgi.annotation.ExportPackage; - -/** The node repository controls and allocates the nodes available in a hosted Vespa zone */ diff --git a/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSourceTest.java b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSourceTest.java index d0d1d61628c..c46677bfc10 100644 --- a/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSourceTest.java +++ b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/ConfigServerFlagSourceTest.java @@ -103,4 +103,4 @@ public class ConfigServerFlagSourceTest { assertFalse(rawFlag2.isPresent()); verify(flagsDb, times(1)).getValue(flagId2); } -}
\ No newline at end of file +} diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/http/FlagsHandlerTest.java index 5ae6ce9820b..cbd37c8a5cf 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/flags/FlagsHandlerTest.java +++ b/configserver-flags/src/test/java/com/yahoo/vespa/configserver/flags/http/FlagsHandlerTest.java @@ -1,25 +1,27 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; +package com.yahoo.vespa.configserver.flags.http; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.text.Utf8; -import com.yahoo.vespa.config.server.http.SessionHandlerTest; +import com.yahoo.vespa.configserver.flags.FlagsDb; import com.yahoo.vespa.configserver.flags.db.FlagsDbImpl; import com.yahoo.vespa.curator.mock.MockCurator; import com.yahoo.vespa.flags.FetchVector; import com.yahoo.vespa.flags.FlagId; import com.yahoo.vespa.flags.Flags; import com.yahoo.vespa.flags.UnboundBooleanFlag; +import com.yahoo.yolean.Exceptions; import org.junit.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import java.util.stream.Stream; -import static com.yahoo.yolean.Exceptions.uncheck; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -37,7 +39,7 @@ public class FlagsHandlerTest { private static final String FLAGS_V1_URL = "https://foo.com:4443/flags/v1"; - private final FlagsDbImpl flagsDb = new FlagsDbImpl(new MockCurator()); + private final FlagsDb flagsDb = new FlagsDbImpl(new MockCurator()); private final FlagsHandler handler = new FlagsHandler(FlagsHandler.testOnlyContext(), flagsDb); @Test @@ -161,7 +163,7 @@ public class FlagsHandlerTest { @Test public void testForcing() { - assertThat(handle(Method.PUT, "/data/" + new FlagId("undef"), "", 404), + assertThat(handle(Method.PUT, "/data/" + new FlagId("undef"), "", 400), containsString("There is no flag 'undef'")); assertThat(handle(Method.PUT, "/data/" + new FlagId("undef") + "?force=true", "", 400), @@ -191,10 +193,12 @@ public class FlagsHandlerTest { HttpResponse response = handler.handle(request); assertEquals(expectedStatus, response.getStatus()); assertEquals("application/json", response.getContentType()); - return uncheck(() -> SessionHandlerTest.getRenderedString(response)); + var outputStream = new ByteArrayOutputStream(); + Exceptions.uncheck(() -> response.render(outputStream)); + return outputStream.toString(StandardCharsets.UTF_8); } private InputStream makeInputStream(String content) { return new ByteArrayInputStream(Utf8.toBytes(content)); } -}
\ No newline at end of file +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java deleted file mode 100644 index 3594c801ca8..00000000000 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/flags/V1Response.java +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.http.flags; - -import com.yahoo.jdisc.Response; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Slime; -import com.yahoo.text.Utf8; -import com.yahoo.vespa.config.SlimeUtils; -import com.yahoo.vespa.config.server.http.HttpConfigResponse; -import com.yahoo.vespa.config.server.http.StaticResponse; - -import java.util.Arrays; -import java.util.List; - -import static com.yahoo.yolean.Exceptions.uncheck; - -/** - * @author hakonhall - */ -public class V1Response extends StaticResponse { - public V1Response(String flagsV1Uri, String... names) { - super(Response.Status.OK, HttpConfigResponse.JSON_CONTENT_TYPE, generateBody(flagsV1Uri, Arrays.asList(names))); - } - - private static String generateBody(String flagsV1Uri, List<String> names) { - Slime slime = new Slime(); - Cursor root = slime.setObject(); - names.forEach(name -> { - Cursor data = root.setObject(name); - data.setString("url", flagsV1Uri + "/" + name); - }); - return Utf8.toString(uncheck(() -> SlimeUtils.toJsonBytes(slime))); - } -} diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml index cfacd6ff8c9..e57024413c9 100644 --- a/configserver/src/main/resources/configserver-app/services.xml +++ b/configserver/src/main/resources/configserver-app/services.xml @@ -92,10 +92,6 @@ <handler id='com.yahoo.vespa.config.server.http.status.StatusHandler' bundle='configserver'> <binding>http://*/status</binding> </handler> - <handler id='com.yahoo.vespa.config.server.http.flags.FlagsHandler' bundle='configserver'> - <binding>http://*/flags/v1</binding> - <binding>http://*/flags/v1/*</binding> - </handler> <handler id='com.yahoo.vespa.config.server.http.v2.TenantHandler' bundle='configserver'> <binding>http://*/application/v2/tenant/</binding> <binding>http://*/application/v2/tenant/*</binding> diff --git a/controller-server/pom.xml b/controller-server/pom.xml index c6c6acafe15..ae756eae1fb 100644 --- a/controller-server/pom.xml +++ b/controller-server/pom.xml @@ -107,6 +107,13 @@ <scope>provided</scope> </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>configserver-flags</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <!-- compile --> <dependency> diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java new file mode 100644 index 00000000000..31058a71816 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java @@ -0,0 +1,30 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.flags; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.configserver.flags.FlagsDb; +import com.yahoo.vespa.configserver.flags.http.FlagsHandler; +import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger; + +/** + * An extension of {@link FlagsHandler} which logs requests to the audit log. + * + * @author mpolden + */ +public class AuditedFlagsHandler extends FlagsHandler { + + private final AuditLogger auditLogger; + + public AuditedFlagsHandler(Context context, Controller controller, FlagsDb flagsDb) { + super(context, flagsDb); + auditLogger = controller.auditLogger(); + } + + @Override + public HttpResponse handle(HttpRequest request) { + return super.handle(auditLogger.log(request)); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java index c7be543dd00..b32cbbcb926 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java @@ -7,7 +7,6 @@ import com.yahoo.application.container.handler.Response; import com.yahoo.component.ComponentSpecification; import com.yahoo.component.Version; import com.yahoo.config.provision.zone.ZoneApi; -import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.container.http.filter.FilterChainRepository; import com.yahoo.jdisc.http.filter.SecurityRequestFilter; import com.yahoo.jdisc.http.filter.SecurityRequestFilterChain; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java index 11aa132b478..83a43287880 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java @@ -60,6 +60,8 @@ public class ControllerContainerTest { " </rotations>\n" + " </config>\n" + " <component id='com.yahoo.vespa.flags.InMemoryFlagSource'/>\n" + + " <component id='com.yahoo.vespa.configserver.flags.db.FlagsDbImpl'/>\n" + + " <component id='com.yahoo.vespa.curator.mock.MockCurator'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.athenz.mock.AthenzClientFactoryMock'/>\n" + " <component id='com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService'/>\n" + @@ -112,6 +114,10 @@ public class ControllerContainerTest { " <binding>http://*/zone/v2</binding>\n" + " <binding>http://*/zone/v2/*</binding>\n" + " </handler>\n" + + " <handler id='com.yahoo.vespa.hosted.controller.restapi.flags.AuditedFlagsHandler'>\n" + + " <binding>http://*/flags/v1</binding>\n" + + " <binding>http://*/flags/v1/*</binding>\n" + + " </handler>\n" + variablePartXml() + "</container>"; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java new file mode 100644 index 00000000000..b4ef98cc7f6 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java @@ -0,0 +1,57 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.flags; + +import com.yahoo.application.container.handler.Request; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.api.AthenzUser; +import com.yahoo.vespa.hosted.controller.auditlog.AuditLog; +import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester; +import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author mpolden + */ +public class AuditedFlagsApiTest extends ControllerContainerTest { + + private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/responses/"; + private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser"); + + private ContainerControllerTester tester; + + @Before + public void before() { + addUserToHostedOperatorRole(operator); + tester = new ContainerControllerTester(container, responses); + } + + @Test + public void test_audit_logging() { + var body = "{\n" + + " \"id\": \"id1\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"value\": true\n" + + " }\n" + + " ]\n" + + "}"; + assertResponse(new Request("http://localhost:8080/flags/v1/data/id1?force=true", body, Request.Method.PUT), + "", 200); + var log = tester.controller().auditLogger().readLog(); + assertEquals(1, log.entries().size()); + var entry = log.entries().get(0); + assertEquals(operator.getFullName(), entry.principal()); + assertEquals(AuditLog.Entry.Method.PUT, entry.method()); + assertEquals("/flags/v1/data/id1?force=true", entry.resource()); + assertEquals(body, log.entries().get(0).data().get()); + } + + private void assertResponse(Request request, String body, int statusCode) { + addIdentityToRequest(request, operator); + tester.assertResponse(request, body, statusCode); + } + +} |