| // 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:cocoon_service/src/foundation/providers.dart'; |
| import 'package:cocoon_service/src/service/buildbucket.dart'; |
| import 'package:cocoon_service/src/service/github_checks_service.dart'; |
| import 'package:cocoon_service/src/service/github_status_service.dart'; |
| import 'package:cocoon_service/src/service/luci_build_service.dart'; |
| import 'package:github/github.dart'; |
| import 'package:googleapis/oauth2/v2.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:meta/meta.dart'; |
| |
| import '../datastore/cocoon_config.dart'; |
| import '../foundation/typedefs.dart'; |
| import '../model/appengine/service_account_info.dart'; |
| import '../model/luci/push_message.dart'; |
| import '../request_handling/body.dart'; |
| import '../request_handling/exceptions.dart'; |
| import '../request_handling/request_handler.dart'; |
| |
| /// An endpoint for listening to LUCI status updates for scheduled builds. |
| /// |
| /// The [ScheduleBuildRequest.notify] property is set to tell LUCI to use our |
| /// PubSub topic. LUCI then publishes updates about build status to that topic, |
| /// which we listen to on the github-updater subscription. When new messages |
| /// arrive, they are posted to this web service. |
| /// |
| /// The PubSub subscription is set up here: |
| /// https://console.cloud.google.com/cloudpubsub/subscription/detail/github-updater?project=flutter-dashboard |
| /// |
| /// This endpoing is responsible for updating GitHub with the status of |
| /// completed builds. |
| /// |
| /// This currently uses the GitHub Status API, but could be refactored at some |
| /// point to use the Checks API, which may offer some more knobs to turn |
| /// on the GitHub page. In particular, it might offer a nice way to retry a |
| /// failed build - which right now would require removing and re-applying the |
| /// label, or pushing a new commit. |
| @immutable |
| class LuciStatusHandler extends RequestHandler<Body> { |
| /// Creates an endpoint for listening to LUCI status updates. |
| const LuciStatusHandler( |
| Config config, |
| this.buildBucketClient, { |
| LoggingProvider loggingProvider, |
| }) : assert(buildBucketClient != null), |
| loggingProvider = loggingProvider ?? Providers.serviceScopeLogger, |
| super(config: config); |
| |
| final BuildBucketClient buildBucketClient; |
| final LoggingProvider loggingProvider; |
| |
| @override |
| Future<Body> post() async { |
| final ServiceAccountInfo serviceAccountInfo = |
| await config.deviceLabServiceAccount; |
| final LuciBuildService luciBuildService = |
| LuciBuildService(config, buildBucketClient, serviceAccountInfo); |
| final GithubStatusService githubStatusService = |
| GithubStatusService(config, luciBuildService); |
| final GithubChecksService githubChecksService = GithubChecksService(config); |
| |
| // Set logger in all the service classes. |
| luciBuildService.setLogger(log); |
| githubChecksService.setLogger(log); |
| |
| if (!await _authenticateRequest(request.headers)) { |
| throw const Unauthorized(); |
| } |
| final String requestString = await utf8.decodeStream(request); |
| final PushMessageEnvelope envelope = PushMessageEnvelope.fromJson( |
| json.decode(requestString) as Map<String, dynamic>, |
| ); |
| final BuildPushMessage buildPushMessage = BuildPushMessage.fromJson( |
| json.decode(envelope.message.data) as Map<String, dynamic>); |
| final Build build = buildPushMessage.build; |
| final String builderName = build.tagsByName('builder').single; |
| final RepositorySlug slug = await config.repoNameForBuilder(builderName); |
| |
| const String shaPrefix = 'sha/git/'; |
| log.debug('Available tags: ${build.tags.toString()}'); |
| // Skip status update if we can not get the sha tag. |
| if (build.tagsByName('buildset').isEmpty) { |
| log.warning('Buildset tag not included, skipping Status Updates'); |
| return Body.empty; |
| } |
| final String sha = build |
| .tagsByName('buildset') |
| .firstWhere((String tag) => tag.startsWith(shaPrefix)) |
| .substring(shaPrefix.length); |
| log.debug('Setting status: ${buildPushMessage.toJson()} for $builderName'); |
| await githubChecksService.updateCheckStatus( |
| buildPushMessage, |
| luciBuildService, |
| slug, |
| ); |
| switch (buildPushMessage.build.status) { |
| case Status.completed: |
| await _markCompleted( |
| sha: sha, |
| builderName: builderName, |
| build: build, |
| githubStatusService: githubStatusService, |
| slug: slug, |
| ); |
| break; |
| case Status.scheduled: |
| case Status.started: |
| final bool success = await githubStatusService.setPendingStatus( |
| ref: sha, |
| builderName: builderName, |
| buildUrl: build.url, |
| slug: slug, |
| ); |
| if (!success) { |
| log.warning('Failed to set status for $builderName'); |
| } |
| break; |
| } |
| return Body.empty; |
| } |
| |
| /// Updates the github status using the push_message [build] sent by LUCI |
| /// as a pub/sub message. |
| Future<void> _markCompleted({ |
| @required String sha, |
| @required String builderName, |
| @required Build build, |
| @required GithubStatusService githubStatusService, |
| @required RepositorySlug slug, |
| }) async { |
| assert(sha != null); |
| assert(builderName != null); |
| assert(build != null); |
| await githubStatusService.setCompletedStatus( |
| ref: sha, |
| builderName: builderName, |
| buildUrl: build.url, |
| result: build.result, |
| slug: slug, |
| ); |
| } |
| |
| Future<bool> _authenticateRequest(HttpHeaders headers) async { |
| final http.Client client = httpClient; |
| final Oauth2Api oauth2api = Oauth2Api(client); |
| final String idToken = headers.value(HttpHeaders.authorizationHeader); |
| if (idToken == null || !idToken.startsWith('Bearer ')) { |
| return false; |
| } |
| final Tokeninfo info = await oauth2api.tokeninfo( |
| idToken: idToken.substring('Bearer '.length), |
| ); |
| if (info.expiresIn == null || info.expiresIn < 1) { |
| return false; |
| } |
| final ServiceAccountInfo devicelabServiceAccount = |
| await config.deviceLabServiceAccount; |
| return info.email == devicelabServiceAccount.email; |
| } |
| } |