blob: 223aef049b86f2d4cee02271e1a91a5740809aa0 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:meta/meta.dart';
import '../model/luci/buildbucket.dart';
import '../request_handling/body.dart';
import 'access_token_provider.dart';
/// A client interface to LUCI BuildBucket
@immutable
class BuildBucketClient {
/// Creates a new build bucket Client.
///
/// The [buildBucketUri] parameter must not be null, and will be defaulted to
/// [kDefaultBuildBucketUri] if not specified.
///
/// The [httpClient] parameter will be defaulted to `HttpClient()` if not
/// specified or null.
BuildBucketClient({
this.buildBucketUri = kDefaultBuildBucketUri,
this.accessTokenService,
HttpClient httpClient,
}) : assert(buildBucketUri != null),
httpClient = httpClient ?? HttpClient();
/// Garbage to prevent browser/JSON parsing exploits.
static const String kRpcResponseGarbage = ")]}'";
/// The default endpoint for BuildBucket requests.
static const String kDefaultBuildBucketUri =
'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds';
/// The base URI for build bucket requests.
///
/// Defaults to [kDefaultBuildBucketUri].
final String buildBucketUri;
/// The token provider for OAuth2 requests.
///
/// If this is non-null, an access token will be attached to any outbound
/// HTTP requests issued by this client.
final AccessTokenService accessTokenService;
/// The [HttpClient] to use for requests.
///
/// Defaults to `HttpClient()`.
final HttpClient httpClient;
Future<T> _postRequest<S extends JsonBody, T>(
String path,
S request,
T responseFromJson(Map<String, dynamic> rawResponse),
) async {
final Uri url = Uri.parse('$buildBucketUri$path');
final HttpClientRequest httpRequest = await httpClient.postUrl(url);
httpRequest.headers.add(HttpHeaders.contentTypeHeader, 'application/json');
httpRequest.headers.add(HttpHeaders.acceptHeader, 'application/json');
if (accessTokenService != null) {
final AccessToken token = await accessTokenService.createAccessToken(
scopes: <String>[
'openid',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
],
);
if (token != null) {
httpRequest.headers.add(
HttpHeaders.authorizationHeader, '${token.type} ${token.data}');
}
}
httpRequest.write(json.encode(request.toJson()));
await httpRequest.flush();
final HttpClientResponse response = await httpRequest.close();
final String rawResponse = await utf8.decodeStream(response);
if (response.statusCode < 300) {
return responseFromJson(
json.decode(rawResponse.substring(kRpcResponseGarbage.length))
as Map<String, dynamic>);
}
throw BuildBucketException(response.statusCode, rawResponse);
}
/// The RPC request to schedule a build.
Future<Build> scheduleBuild(ScheduleBuildRequest request) {
return _postRequest<ScheduleBuildRequest, Build>(
'/ScheduleBuild',
request,
Build.fromJson,
);
}
/// The RPC request to search for builds.
Future<SearchBuildsResponse> searchBuilds(SearchBuildsRequest request) {
return _postRequest<SearchBuildsRequest, SearchBuildsResponse>(
'/SearchBuilds',
request,
SearchBuildsResponse.fromJson,
);
}
/// The RPC method to batch multiple RPC methods in a single HTTP request.
///
/// The response is guaranteed to contain line-item responses for all
/// line-item requests that were issued in [request]. If only a subset of
/// responses were retrieved, a [BatchRequestException] will be thrown.
Future<BatchResponse> batch(BatchRequest request) async {
final BatchResponse response =
await _postRequest<BatchRequest, BatchResponse>(
'/Batch',
request,
BatchResponse.fromJson,
);
if (response.responses.length != request.requests.length) {
throw BatchRequestException('Failed to execute all requests');
}
return response;
}
/// The RPC request to cancel a build.
Future<Build> cancelBuild(CancelBuildRequest request) {
return _postRequest<CancelBuildRequest, Build>(
'/CancelBuild',
request,
Build.fromJson,
);
}
/// The RPC request to get details about a build.
Future<Build> getBuild(GetBuildRequest request) {
return _postRequest<GetBuildRequest, Build>(
'/GetBuild',
request,
Build.fromJson,
);
}
/// Closes the underlying [HttpClient].
///
/// If `force` is true, it will close immediately and cause outstanding
/// requests to end with an error. Otherwise, it will wait for outstanding
/// requests to finish before closing.
///
/// Once this call completes, additional RPC requests will throw an exception.
void close({bool force = false}) {
httpClient.close(force: force);
}
}
class BuildBucketException implements Exception {
const BuildBucketException(this.statusCode, this.message);
/// The HTTP status code of the error.
final int statusCode;
/// The message from the server.
final String message;
@override
String toString() => '$runtimeType: [$statusCode]: $message';
}
class BatchRequestException implements Exception {
BatchRequestException(this.message);
final String message;
@override
String toString() => message;
}