aboutsummaryrefslogtreecommitdiffstats
path: root/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationApiHandler.java
blob: b33b21691afc32b258b07e4f3018ed6581162402 (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
// 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.v2;

import com.yahoo.cloud.config.ConfigserverConfig;
import com.yahoo.component.annotation.Inject;
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.jdisc.utils.MultiPartFormParser;
import com.yahoo.container.jdisc.utils.MultiPartFormParser.PartItem;
import com.yahoo.jdisc.application.BindingMatch;
import com.yahoo.jdisc.http.HttpHeaders;
import com.yahoo.vespa.config.server.ApplicationRepository;
import com.yahoo.vespa.config.server.application.CompressedApplicationInputStream;
import com.yahoo.vespa.config.server.http.BadRequestException;
import com.yahoo.vespa.config.server.http.SessionHandler;
import com.yahoo.vespa.config.server.http.Utils;
import com.yahoo.vespa.config.server.http.v2.response.SessionPrepareAndActivateResponse;
import com.yahoo.vespa.config.server.session.PrepareParams;
import com.yahoo.vespa.config.server.tenant.TenantRepository;
import com.yahoo.yolean.Exceptions;
import org.apache.hc.core5.http.ContentType;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static com.yahoo.vespa.config.server.application.CompressedApplicationInputStream.createFromCompressedStream;
import static com.yahoo.vespa.config.server.http.Utils.checkThatTenantExists;
import static com.yahoo.vespa.config.server.http.v2.SessionCreateHandler.validateDataAndHeader;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;

/**
 *  * The implementation of the /application/v2 API.
 *
 *
 * @author hmusum
 */
// TODO: only handles 'prepareandactive' for now, merge other handlers into this one
public class ApplicationApiHandler extends SessionHandler {

    public final static String APPLICATION_X_GZIP = "application/x-gzip";
    public final static String APPLICATION_ZIP = "application/zip";
    public final static String MULTIPART_FORM_DATA = "multipart/form-data";
    public final static String MULTIPART_PARAMS = "prepareParams";
    public final static String MULTIPART_APPLICATION_PACKAGE = "applicationPackage";
    public final static String contentTypeHeader = "Content-Type";

    private final TenantRepository tenantRepository;
    private final Duration zookeeperBarrierTimeout;
    private final Zone zone;
    private final long maxApplicationPackageSize;

    @Inject
    public ApplicationApiHandler(Context ctx,
                                 ApplicationRepository applicationRepository,
                                 ConfigserverConfig configserverConfig,
                                 Zone zone) {
        super(ctx, applicationRepository);
        this.tenantRepository = applicationRepository.tenantRepository();
        this.zookeeperBarrierTimeout = Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout());
        this.maxApplicationPackageSize = configserverConfig.maxApplicationPackageSize();
        this.zone = zone;
    }

    @Override
    protected HttpResponse handlePOST(HttpRequest request) {
        validateDataAndHeader(request, List.of(APPLICATION_X_GZIP, APPLICATION_ZIP, MULTIPART_FORM_DATA));
        TenantName tenantName = validateTenant(request);

        PrepareParams prepareParams;
        CompressedApplicationInputStream compressedStream;
        boolean multipartRequest = Optional.ofNullable(request.getHeader(HttpHeaders.Names.CONTENT_TYPE))
                .map(ContentType::parse)
                .map(contentType -> contentType.getMimeType().equalsIgnoreCase(MULTIPART_FORM_DATA))
                .orElse(false);
        if (multipartRequest) {
            Map<String, PartItem> parts = Map.of();
            try {
                parts = new MultiPartFormParser(request).readParts();
                byte[] params;
                try (InputStream part = parts.get(MULTIPART_PARAMS).data()) { params = part.readAllBytes(); }
                log.log(FINE, "Deploy parameters: [{0}]", new String(params, StandardCharsets.UTF_8));
                prepareParams = PrepareParams.fromJson(params, tenantName, zookeeperBarrierTimeout);
                PartItem appPackagePart = parts.get(MULTIPART_APPLICATION_PACKAGE);
                compressedStream = createFromCompressedStream(appPackagePart.data(), appPackagePart.contentType(), maxApplicationPackageSize);
            } catch (IOException e) {
                // Multipart exception happens when controller abandons the request due to other exceptions while deploying.
                log.log(e instanceof MultiPartFormParser.MultiPartException ? FINE : WARNING,
                        "Unable to parse multipart in deploy from tenant '" + tenantName.value() + "': " + Exceptions.toMessageString(e));

                var message = "Deploy request from '" + tenantName.value() + "' contains invalid data: " + e.getMessage();
                log.log(INFO, message + ", parts: " + parts, e);
                throw new BadRequestException(message);
            }
        } else {
            prepareParams = PrepareParams.fromHttpRequest(request, tenantName, zookeeperBarrierTimeout);
            compressedStream = createFromCompressedStream(request.getData(), request.getHeader(contentTypeHeader), maxApplicationPackageSize);
        }

        // Aid debugging by adding full application id to access log (since only tenant name is part of the request URI path)
        request.getAccessLogEntry()
                .ifPresent(e -> e.addKeyValue("app.id", prepareParams.getApplicationId().toFullString()));

        try (compressedStream) {
            PrepareResult result = applicationRepository.deploy(compressedStream, prepareParams);
            return new SessionPrepareAndActivateResponse(result, request, prepareParams.getApplicationId(), zone);
        }
        catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Duration getTimeout() {
        return zookeeperBarrierTimeout.plus(Duration.ofSeconds(180));
    }

    private TenantName validateTenant(HttpRequest request) {
        TenantName tenantName = getTenantNameFromRequest(request);
        checkThatTenantExists(tenantRepository, tenantName);
        return tenantName;
    }

    public static TenantName getTenantNameFromRequest(HttpRequest request) {
        BindingMatch<?> bm = Utils.getBindingMatch(request, "http://*/application/v2/tenant/*/prepareandactivate*");
        return TenantName.from(bm.group(2));
    }

}