blob: faef14dfd36f50938f1794bd16686ab070fe23ab [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: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;
}
}