| // 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:http/http.dart' as http; |
| import 'package:meta/meta.dart'; |
| |
| import 'package:buildbucket/buildbucket_pb.dart' as bbv2; |
| import 'access_token_provider.dart'; |
| |
| import '../service/logging.dart'; |
| |
| // TODO generalize the two clients to remove this. |
| /// A client interface to LUCI BuildBucket |
| @immutable |
| class BuildBucketV2Client { |
| /// 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. |
| BuildBucketV2Client({ |
| this.buildBucketBuilderUri = kDefaultBuildBucketBuilderUri, |
| this.buildBucketBuildUri = kDefaultBuildBucketBuildUri, |
| this.accessTokenService, |
| http.Client? httpClient, |
| }) : httpClient = httpClient ?? http.Client(); |
| |
| /// Garbage to prevent browser/JSON parsing exploits. |
| static const String kRpcResponseGarbage = ")]}'"; |
| |
| /// The default endpoint for BuildBucket build requests. |
| static const String kDefaultBuildBucketBuildUri = 'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds'; |
| |
| /// The default endpoint for BuildBucket builder requests. |
| static const String kDefaultBuildBucketBuilderUri = 'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builders'; |
| |
| /// The base URI for build bucket requests. |
| /// |
| /// Defaults to [kDefaultBuildBucketBuildUri]. |
| final String buildBucketBuildUri; |
| |
| /// The base URI for build bucket requests. |
| /// |
| /// Defaults to [kDefaultBuildBucketBuilderUri]. |
| final String buildBucketBuilderUri; |
| |
| /// 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 [http.Client] to use for requests. |
| final http.Client httpClient; |
| |
| Future<String> _postRequest( |
| String path, |
| String request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final Uri url = Uri.parse('$buildBucketUri$path'); |
| final AccessToken? token = await accessTokenService?.createAccessToken(); |
| |
| log.info('Making bbv2 request with path: $url and body: $request'); |
| |
| final http.Response response = await httpClient.post( |
| url, |
| body: request, |
| headers: <String, String>{ |
| HttpHeaders.contentTypeHeader: 'application/json', |
| HttpHeaders.acceptHeader: 'application/json', |
| if (token != null) HttpHeaders.authorizationHeader: '${token.type} ${token.data}', |
| }, |
| ); |
| |
| log.info('bbv2 request returned response code ${response.statusCode}'); |
| log.info('bbv2 request response body: ${response.body}'); |
| |
| if (response.statusCode < 300) { |
| return response.body.substring(kRpcResponseGarbage.length); |
| } |
| throw BuildBucketException(response.statusCode, response.body); |
| } |
| |
| /// The RPC request to schedule a build. |
| Future<bbv2.Build> scheduleBuild( |
| bbv2.ScheduleBuildRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final bbv2.Build build = bbv2.Build.create(); |
| |
| final String responseBody = await _postRequest( |
| '/ScheduleBuild', |
| jsonEncode(request.toProto3Json()), |
| buildBucketUri: buildBucketUri, |
| ); |
| |
| build.mergeFromProto3Json(jsonDecode(responseBody)); |
| return build; |
| } |
| |
| /// The RPC request to search for builds. |
| Future<bbv2.SearchBuildsResponse> searchBuilds( |
| bbv2.SearchBuildsRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final bbv2.SearchBuildsResponse searchBuildsResponse = bbv2.SearchBuildsResponse.create(); |
| |
| final String responseBody = await _postRequest( |
| '/SearchBuilds', |
| jsonEncode(request.toProto3Json()), |
| buildBucketUri: buildBucketUri, |
| ); |
| |
| searchBuildsResponse.mergeFromProto3Json(jsonDecode(responseBody)); |
| return searchBuildsResponse; |
| } |
| |
| /// 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<bbv2.BatchResponse> batch( |
| bbv2.BatchRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final bbv2.BatchResponse response = bbv2.BatchResponse.create(); |
| final String responseBody = await _postRequest( |
| '/Batch', |
| //For some reason this needs to be stringified as the proto message is not quoted for some reason. |
| jsonEncode(request.toProto3Json()), |
| // this needs to use an object with mergeFromProto3Json, cannot use fromJson here. |
| buildBucketUri: buildBucketUri, |
| ); |
| response.mergeFromProto3Json(jsonDecode(responseBody)); |
| if (response.responses.length != request.requests.length) { |
| throw BatchRequestException('Failed to execute all requests'); |
| } |
| log.info('Batch response matches request size.'); |
| return response; |
| } |
| |
| /// The RPC request to cancel a build. |
| Future<bbv2.Build> cancelBuild( |
| bbv2.CancelBuildRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final bbv2.Build build = bbv2.Build.create(); |
| |
| final String responseBody = await _postRequest( |
| '/CancelBuild', |
| jsonEncode(request.toProto3Json()), |
| buildBucketUri: buildBucketUri, |
| ); |
| |
| build.mergeFromProto3Json(jsonDecode(responseBody)); |
| return build; |
| } |
| |
| /// The RPC request to get details about a build. |
| Future<bbv2.Build> getBuild( |
| bbv2.GetBuildRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuildUri, |
| }) async { |
| final bbv2.Build build = bbv2.Build.create(); |
| |
| final String responseBody = await _postRequest( |
| '/GetBuild', |
| jsonEncode(request.toProto3Json()), |
| buildBucketUri: buildBucketUri, |
| ); |
| |
| build.mergeFromProto3Json(jsonDecode(responseBody)); |
| return build; |
| } |
| |
| /// The RPC request to get a list of builders. |
| Future<bbv2.ListBuildersResponse> listBuilders( |
| bbv2.ListBuildersRequest request, { |
| String buildBucketUri = kDefaultBuildBucketBuilderUri, |
| }) async { |
| final bbv2.ListBuildersResponse listBuildersResponse = bbv2.ListBuildersResponse.create(); |
| |
| final String responseBody = await _postRequest( |
| '/ListBuilders', |
| jsonEncode(request.toProto3Json()), |
| buildBucketUri: buildBucketUri, |
| ); |
| |
| listBuildersResponse.mergeFromProto3Json(jsonDecode(responseBody)); |
| return listBuildersResponse; |
| } |
| |
| /// Closes the underlying [HttpClient]. |
| /// |
| /// Once this call completes, additional RPC requests will throw an exception. |
| void close() { |
| httpClient.close(); |
| } |
| } |
| |
| 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; |
| } |