aboutsummaryrefslogtreecommitdiffstats
path: root/configserver-flags/src/main/java/com/yahoo/vespa/configserver/flags/http/FlagsHandler.java
blob: 27a01a6bf40fae34efdf08f409390b4f748a5793 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright Vespa.ai. 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.component.annotation.Inject;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.Path;
import com.yahoo.vespa.configserver.flags.FlagsDb;
import com.yahoo.vespa.flags.FlagDefinition;
import com.yahoo.vespa.flags.FlagId;
import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.flags.json.FlagData;
import com.yahoo.yolean.Exceptions;

import java.io.UncheckedIOException;
import java.net.URI;
import java.util.Objects;
import java.util.logging.Level;

/**
 * Handles /flags/v1 requests
 *
 * @author hakonhall
 */
public class FlagsHandler extends ThreadedHttpRequestHandler {

    private final FlagsDb flagsDb;

    @Inject
    public FlagsHandler(ThreadedHttpRequestHandler.Context context, FlagsDb flagsDb) {
        super(context);
        this.flagsDb = flagsDb;
    }

    @Override
    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(Level.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));
        return ErrorResponse.notFoundError("Nothing at path '" + path + "'");
    }

    private HttpResponse handlePUT(HttpRequest request) {
        Path path = new Path(request.getUri());
        if (path.matches("/flags/v1/data/{flagId}")) return putFlagData(request, findFlagId(request, path));
        return ErrorResponse.notFoundError("Nothing at path '" + path + "'");
    }

    private HttpResponse handleDELETE(HttpRequest request) {
        Path path = new Path(request.getUri());
        if (path.matches("/flags/v1/data/{flagId}")) return deleteFlagData(findFlagId(request, path));
        return ErrorResponse.notFoundError("Nothing at path '" + path + "'");
    }

    private String flagsV1Uri(HttpRequest request) {
        URI uri = request.getUri();
        String port = uri.getPort() < 0 ? "" : ":" + uri.getPort();
        return uri.getScheme() + "://" + uri.getHost() + port + "/flags/v1";
    }

    private HttpResponse getDefinedFlag(FlagId flagId) {
        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.getAllFlagData(),
                                        Objects.equals(request.getProperty("recursive"), "true"));
    }

    private HttpResponse getFlagData(FlagId flagId) {
        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) {
        FlagData data;
        try {
            data = FlagData.deserialize(request.getData());
        } catch (UncheckedIOException e) {
            return ErrorResponse.badRequest("Failed to deserialize request data: " + Exceptions.toMessageString(e));
        }

        if (!isForce(request)) {
            FlagDefinition definition = Flags.getFlag(flagId).get(); // FlagId has been validated in findFlagId()
            data.validate(definition.getUnboundFlag().serializer());
        }

        flagsDb.setValue(flagId, data);
        return new OKResponse();
    }

    private HttpResponse deleteFlagData(FlagId flagId) {
        flagsDb.removeValue(flagId);
        return new OKResponse();
    }

    private FlagId findFlagId(HttpRequest request, Path path) {
        FlagId flagId = new FlagId(path.get("flagId"));
        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");
    }

}