aboutsummaryrefslogtreecommitdiffstats
path: root/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java
blob: e043afdbf43d850aefc72716799ebd7dd7b570bc (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright Yahoo. 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.base.Predicate;
import com.google.common.collect.Iterables;
import com.yahoo.collections.Tuple2;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.jdisc.application.BindingMatch;
import com.yahoo.vespa.config.ConfigKey;
import com.yahoo.vespa.config.GetConfigRequest;
import com.yahoo.vespa.config.PayloadChecksums;
import com.yahoo.vespa.config.protocol.DefContent;
import com.yahoo.vespa.config.protocol.VespaVersion;
import com.yahoo.vespa.config.server.RequestHandler;
import com.yahoo.vespa.config.server.http.v2.request.HttpConfigRequests;
import com.yahoo.vespa.config.server.http.v2.request.TenantRequest;
import com.yahoo.vespa.config.util.ConfigUtils;

import java.util.Collections;
import java.util.Optional;
import java.util.Set;

/**
 * A request to get config, bound to tenant and app id. Used by both v1 and v2 of the config REST API.
 *
 * @author Ulf Lilleengen
 */
public class HttpConfigRequest implements GetConfigRequest, TenantRequest {

    private static final String NOCACHE = "noCache";
    private static final String REQUIRED_GENERATION = "requiredGeneration";
    private final ConfigKey<?> key;
    private final ApplicationId appId;
    private final boolean noCache;
    private final Optional<Long> requiredGeneration;

    private HttpConfigRequest(ConfigKey<?> key, ApplicationId appId, boolean noCache, Optional<Long> requiredGeneration) {
        this.key = key;
        this.appId = appId;
        this.noCache = noCache;
        this.requiredGeneration = requiredGeneration;
    }

    private static ConfigKey<?> fromRequestV1(HttpRequest req) {
        BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*"); // see jdisc-bindings.cfg
        String conf = bm.group(2); // The port number is implicitly 1, it seems
        String cId;
        String cName;
        String cNamespace;
        if (bm.groupCount() >= 4) {
            cId = bm.group(3);
        } else {
            cId = "";
        }
        Tuple2<String, String> nns = nameAndNamespace(conf);
        cName = nns.first;
        cNamespace = nns.second;
        return new ConfigKey<>(cName, cId, cNamespace);
    }

    public static HttpConfigRequest createFromRequestV1(HttpRequest req) {
        return new HttpConfigRequest(fromRequestV1(req),
                                     ApplicationId.defaultId(),
                                     req.getBooleanProperty(NOCACHE),
                                     requiredGeneration(req));
    }

    public static HttpConfigRequest createFromRequestV2(HttpRequest req) {
        // Four bindings for this: with full app id or only name, with and without config id (like v1)
        BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req,
                "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*",
                "http://*/config/v2/tenant/*/application/*/*/*");
        if (bm.groupCount() > 6) return createFromRequestV2FullAppId(req, bm);
        return createFromRequestV2SimpleAppId(req, bm);
    }

    // The URL pattern with only tenant and application given
    private static HttpConfigRequest createFromRequestV2SimpleAppId(HttpRequest req, BindingMatch<?> bm) {
        String cId;
        String cName;
        String cNamespace;
        TenantName tenant = TenantName.from(bm.group(2));
        ApplicationName application = ApplicationName.from(bm.group(3));
        String conf = bm.group(4);
        if (bm.groupCount() >= 6) {
            cId = bm.group(5);
        } else {
            cId = "";
        }
        Tuple2<String, String> nns = nameAndNamespace(conf);
        cName = nns.first;
        cNamespace = nns.second;
        return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace),
                                     new ApplicationId.Builder().applicationName(application).tenant(tenant).build(),
                                     req.getBooleanProperty(NOCACHE),
                                     requiredGeneration(req));
    }

    // The URL pattern with full app id given
    private static HttpConfigRequest createFromRequestV2FullAppId(HttpRequest req, BindingMatch<?> bm) {
        String cId;
        String cName;
        String cNamespace;
        String tenant = bm.group(2);
        String application = bm.group(3);
        String environment = bm.group(4);
        String region = bm.group(5);
        String instance = bm.group(6);
        String conf = bm.group(7);
        if (bm.groupCount() >= 9) {
            cId = bm.group(8);
        } else {
            cId = "";
        }
        Tuple2<String, String> nns = nameAndNamespace(conf);
        cName = nns.first;
        cNamespace = nns.second;

        ApplicationId appId = new ApplicationId.Builder()
                              .tenant(tenant)
                              .applicationName(application)
                              .instanceName(instance)
                              .build();
        return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace),
                                     appId,
                                     req.getBooleanProperty(NOCACHE),
                                     requiredGeneration(req));
    }

    /**
     * Throws an exception if bad config or config id
     *
     * @param requestKey     a {@link com.yahoo.vespa.config.ConfigKey}
     * @param requestHandler a {@link RequestHandler}
     * @param appId appId
     */
    public static void validateRequestKey(ConfigKey<?> requestKey, RequestHandler requestHandler, ApplicationId appId) {
        Set<ConfigKey<?>> allConfigsProduced = requestHandler.allConfigsProduced(appId, Optional.empty());
        if (allConfigsProduced.isEmpty()) {
            // This will happen if the configserver is starting up, but has not built a config model
            throwModelNotReady();
        }
        if (configNameNotFound(requestKey, allConfigsProduced)) {
            throw new NotFoundException("No such config: " + requestKey.getNamespace() + "." + requestKey.getName());
        }
        if (configIdNotFound(requestHandler, requestKey, appId)) {
            throw new NotFoundException("No such config id: " + requestKey.getConfigId());
        }
    }    
    
    public static void throwModelNotReady() {
        throw new NotFoundException("Config not available, verify that an application package has been deployed and activated.");
    }

    public static void throwPreconditionFailed(long requiredGeneration) {
        throw new PreconditionFailedException("Config for required generation " + requiredGeneration + " could not be found.");
    }

    /**
     * If the given config is produced by the model at all
     *
     * @return ok or not
     */
    private static boolean configNameNotFound(final ConfigKey<?> requestKey, Set<ConfigKey<?>> allConfigsProduced) {
        return !Iterables.any(allConfigsProduced, new Predicate<>() {
            @Override
            public boolean apply(ConfigKey<?> k) {
                return k.getName().equals(requestKey.getName()) && k.getNamespace().equals(requestKey.getNamespace());
            }
        });
    }

    private static boolean configIdNotFound(RequestHandler requestHandler, ConfigKey<?> requestKey, ApplicationId appId) {
      return !requestHandler.allConfigIds(appId, Optional.empty()).contains(requestKey.getConfigId());
    }
    
    public static Tuple2<String, String> nameAndNamespace(String nsDotName) {
        Tuple2<String, String> ret = ConfigUtils.getNameAndNamespaceFromString(nsDotName);
        if ("".equals(ret.second)) throw new IllegalArgumentException("Illegal config, must be of form namespace.name.");
        return ret;
    }

    @Override
    public ConfigKey<?> getConfigKey() {
        return key;
    }

    @Override
    public DefContent getDefContent() {
        return DefContent.fromList(Collections.emptyList());
    }

    @Override
    public Optional<VespaVersion> getVespaVersion() {
        return Optional.empty();
    }

    @Override
    public ApplicationId getApplicationId() {
        return appId;
    }

    public boolean noCache() {
        return noCache;
    }

    @Override
    public String getRequestDefMd5() { return ConfigUtils.getDefMd5(getDefContent().asList()); }

    @Override
    public PayloadChecksums configPayloadChecksums() { return PayloadChecksums.empty(); }

    public Optional<Long> requiredGeneration() { return requiredGeneration; }

    static Optional<Long> requiredGeneration(HttpRequest req) {
        Optional<String> requiredGeneration = Optional.ofNullable(req.getProperty(REQUIRED_GENERATION));
        return requiredGeneration.map(Long::parseLong);
    }

}